From 4fa3ca643f6f9d84b0c7c301eeb5435177eaf617 Mon Sep 17 00:00:00 2001 From: Alisa Mylnikova Date: Thu, 26 Apr 2018 16:03:09 +0700 Subject: [PATCH 1/9] Fix #172: Add support for fill-rule --- Source/model/geom2d/Path.swift | 8 +++++++- Source/render/ShapeRenderer.swift | 15 ++++++++++----- Source/svg/SVGParser.swift | 21 +++++++++++++++++++-- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/Source/model/geom2d/Path.swift b/Source/model/geom2d/Path.swift index e125738f..52ee970f 100644 --- a/Source/model/geom2d/Path.swift +++ b/Source/model/geom2d/Path.swift @@ -1,11 +1,17 @@ import Foundation +public enum FillRule { + case nonzero, evenodd +} + open class Path: Locus { open let segments: [PathSegment] + open let fillRule: FillRule - public init(segments: [PathSegment] = []) { + public init(segments: [PathSegment] = [], fillRule: FillRule = .nonzero) { self.segments = segments + self.fillRule = fillRule } override open func bounds() -> Rect { diff --git a/Source/render/ShapeRenderer.swift b/Source/render/ShapeRenderer.swift index f519df4f..d9cb4d92 100644 --- a/Source/render/ShapeRenderer.swift +++ b/Source/render/ShapeRenderer.swift @@ -38,7 +38,12 @@ class ShapeRenderer: NodeRenderer { if shape.fill != nil || shape.stroke != nil { setGeometry(shape.form, ctx: ctx.cgContext!) - drawPath(shape.fill, stroke: shape.stroke, ctx: ctx.cgContext!, opacity: opacity) + + var fillRule = FillRule.nonzero + if let path = shape.form as? Path { + fillRule = path.fillRule + } + drawPath(shape.fill, stroke: shape.stroke, ctx: ctx.cgContext!, opacity: opacity, fillRule: fillRule) } } @@ -102,7 +107,7 @@ class ShapeRenderer: NodeRenderer { return CGRect(x: CGFloat(rect.x), y: CGFloat(rect.y), width: CGFloat(rect.w), height: CGFloat(rect.h)) } - fileprivate func drawPath(_ fill: Fill?, stroke: Stroke?, ctx: CGContext?, opacity: Double) { + fileprivate func drawPath(_ fill: Fill?, stroke: Stroke?, ctx: CGContext?, opacity: Double, fillRule: FillRule) { var shouldStrokePath = false if fill is Gradient || stroke?.fill is Gradient { shouldStrokePath = true @@ -112,15 +117,15 @@ class ShapeRenderer: NodeRenderer { let path = ctx!.path setFill(fill, ctx: ctx, opacity: opacity) if stroke.fill is Gradient && !(fill is Gradient) { - ctx!.drawPath(using: .fill) + ctx!.drawPath(using: fillRule == .nonzero ? .fill : .eoFill) } - drawWithStroke(stroke, ctx: ctx, opacity: opacity, shouldStrokePath: shouldStrokePath, path: path, mode: .fillStroke) + drawWithStroke(stroke, ctx: ctx, opacity: opacity, shouldStrokePath: shouldStrokePath, path: path, mode: fillRule == .nonzero ? .fillStroke : .eoFillStroke) return } if let fill = fill { setFill(fill, ctx: ctx, opacity: opacity) - ctx!.drawPath(using: .fill) + ctx!.drawPath(using: fillRule == .nonzero ? .fill : .eoFill) return } diff --git a/Source/svg/SVGParser.swift b/Source/svg/SVGParser.swift index f8a4f1c3..d315b03c 100644 --- a/Source/svg/SVGParser.swift +++ b/Source/svg/SVGParser.swift @@ -42,7 +42,7 @@ open class SVGParser { } let availableStyleAttributes = ["stroke", "stroke-width", "stroke-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", - "fill", "text-anchor", "clip-path", "fill-opacity", + "fill", "fill-rule", "text-anchor", "clip-path", "fill-opacity", "stop-color", "stop-opacity", "font-family", "font-size", "font-weight", "opacity", "color", "visibility"] @@ -273,7 +273,10 @@ open class SVGParser { let position = getPosition(element) switch element.name { case "path": - if let path = parsePath(node) { + if var path = parsePath(node) { + if let rule = getFillRule(styleAttributes) { + path = Path(segments: path.segments, fillRule: rule) + } return Shape(form: path, fill: getFillColor(styleAttributes, groupStyle: styleAttributes), stroke: getStroke(styleAttributes, groupStyle: styleAttributes), place: position, opacity: getOpacity(styleAttributes), clip: getClipPath(styleAttributes), tag: getTag(element)) } case "line": @@ -1335,6 +1338,20 @@ open class SVGParser { } return false } + + fileprivate func getFillRule(_ attributes: [String: String]) -> FillRule? { + if let rule = attributes["fill-rule"] { + switch rule { + case "nonzero": + return .nonzero + case "evenodd": + return .evenodd + default: + return .none + } + } + return .none + } fileprivate func copyNode(_ referenceNode: Node) -> Node? { let pos = referenceNode.place From 3e3bbe6d0c0ac728c3523f6c539b288a54a31140 Mon Sep 17 00:00:00 2001 From: Alisa Mylnikova Date: Fri, 27 Apr 2018 16:42:03 +0700 Subject: [PATCH 2/9] Fix #335: Parse generic fonts from svg --- Source/render/RenderUtils.swift | 32 +++++++++++++++++--------------- Source/render/TextRenderer.swift | 20 ++++++++++---------- Source/svg/SVGParser.swift | 2 +- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/Source/render/RenderUtils.swift b/Source/render/RenderUtils.swift index 9717fec2..82e06599 100644 --- a/Source/render/RenderUtils.swift +++ b/Source/render/RenderUtils.swift @@ -90,26 +90,28 @@ class RenderUtils { fatalError("Unsupported node: \(node)") } + static let availableFonts = UIFont.familyNames.map{ $0.lowercased() } + class func loadFont(name: String, size: Int) -> MFont? { - let separationSet = CharacterSet(charactersIn: ",") - let names = name.components(separatedBy: separationSet) - var customFont: MFont? = .none - names.forEach { fontName in - if customFont != .none { - return + + let fontPriorities = name.split(separator: ",").map{ String($0).trimmingCharacters(in: CharacterSet(charactersIn: " '")).lowercased() } + for font in fontPriorities { + if availableFonts.contains(font) { + return MFont(name: font, size: CGFloat(size)) } - - if fontName.first == " " { - let index = fontName.index(fontName.startIndex, offsetBy: 1) - let fixedName = String(fontName.suffix(from: index)) - customFont = MFont(name: fixedName, size: CGFloat(size)) - return + + if name == "serif" { + return MFont(name: "Georgia", size: CGFloat(size)) + } + if name == "sans-serif" { + return MFont(name: "Arial", size: CGFloat(size)) + } + if name == "monospace" { + return MFont(name: "Courier", size: CGFloat(size)) } - - customFont = MFont(name: fontName, size: CGFloat(size)) } - return customFont + return .none } class func applyOpacity(_ color: Color, opacity: Double) -> Color { diff --git a/Source/render/TextRenderer.swift b/Source/render/TextRenderer.swift index 81a0ac30..45760dd5 100644 --- a/Source/render/TextRenderer.swift +++ b/Source/render/TextRenderer.swift @@ -79,18 +79,18 @@ class TextRenderer: NodeRenderer { guard let text = text else { return MFont.systemFont(ofSize: 18.0) } - - if let textFont = text.font { - if let customFont = RenderUtils.loadFont(name: textFont.name, size: textFont.size) { - return customFont - } else { - if let weight = getWeight(textFont.weight) { - return MFont.systemFont(ofSize: CGFloat(textFont.size), weight: weight) - } - return MFont.systemFont(ofSize: CGFloat(textFont.size)) + guard let textFont = text.font else { + return MFont.systemFont(ofSize: MFont.mSystemFontSize) + } + + if let customFont = RenderUtils.loadFont(name: textFont.name, size: textFont.size) { + return customFont + } else { + if let weight = getWeight(textFont.weight) { + return MFont.systemFont(ofSize: CGFloat(textFont.size), weight: weight) } + return MFont.systemFont(ofSize: CGFloat(textFont.size)) } - return MFont.systemFont(ofSize: MFont.mSystemFontSize) } fileprivate func getWeight(_ weight: String) -> MFont.Weight? { diff --git a/Source/svg/SVGParser.swift b/Source/svg/SVGParser.swift index f8a4f1c3..6dd27ed2 100644 --- a/Source/svg/SVGParser.swift +++ b/Source/svg/SVGParser.swift @@ -1268,7 +1268,7 @@ open class SVGParser { } fileprivate func getFontName(_ attributes: [String: String]) -> String? { - return attributes["font-family"] + return attributes["font-family"]?.trimmingCharacters(in: .whitespacesAndNewlines) } fileprivate func getFontSize(_ attributes: [String: String]) -> Int? { From 5c538f8c59d6cf19c5baef67394a78f0823879d2 Mon Sep 17 00:00:00 2001 From: Alisa Mylnikova Date: Fri, 27 Apr 2018 16:52:23 +0700 Subject: [PATCH 3/9] MacOS adaptation --- Source/render/RenderUtils.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/render/RenderUtils.swift b/Source/render/RenderUtils.swift index 82e06599..556ed68e 100644 --- a/Source/render/RenderUtils.swift +++ b/Source/render/RenderUtils.swift @@ -90,7 +90,7 @@ class RenderUtils { fatalError("Unsupported node: \(node)") } - static let availableFonts = UIFont.familyNames.map{ $0.lowercased() } + static let availableFonts = MFont.familyNames.map{ $0.lowercased() } class func loadFont(name: String, size: Int) -> MFont? { From 5f66f685685dc65b0a98a0e805227cbe6c849fc2 Mon Sep 17 00:00:00 2001 From: Yuri Strot Date: Fri, 27 Apr 2018 19:18:08 +0700 Subject: [PATCH 4/9] Minor fixes in JSON serialization --- MacawTests/MacawSVGTests.swift | 2 - MacawTests/SceneSerialization.swift | 69 ++++++++++------------------- 2 files changed, 23 insertions(+), 48 deletions(-) diff --git a/MacawTests/MacawSVGTests.swift b/MacawTests/MacawSVGTests.swift index 904afaba..90cd8e33 100644 --- a/MacawTests/MacawSVGTests.swift +++ b/MacawTests/MacawSVGTests.swift @@ -154,8 +154,6 @@ class MacawSVGTests: XCTestCase { validate("triangle") } - - func validateJSON(node: Node, referenceFile: String) { let bundle = Bundle(for: type(of: TestUtils())) diff --git a/MacawTests/SceneSerialization.swift b/MacawTests/SceneSerialization.swift index 7631deeb..43ab00a1 100644 --- a/MacawTests/SceneSerialization.swift +++ b/MacawTests/SceneSerialization.swift @@ -21,26 +21,22 @@ func parse(_ from: Any?) -> T { return from as? T ?? T() } - -internal protocol Serializable { +protocol Serializable { func toDictionary() -> [String:Any] } - - class NodeSerializer { var factories = [String:([String:Any]) -> Node]() - static var shared = NodeSerializer() + let locusSerializer = LocusSerializer() init() { factories["Shape"] = { dictionary in let locusDict = dictionary["form"] as! [String:Any] - let locus = LocusSerializer.shared.instance(dictionary: locusDict) + let locus = self.locusSerializer.instance(dictionary: locusDict) let shape = Shape(form: locus) - shape.fromDictionary(dictionary: dictionary) // fill in the fields inherited from Node if let fillDict = dictionary["fill"] as? [String:Any], let fillType = fillDict["type"] as? String, fillType == "Color" { shape.fill = Color(dictionary: fillDict) @@ -54,7 +50,6 @@ class NodeSerializer { factories["Text"] = { dictionary in let textString = dictionary["text"] as! String let text = Text(text: textString) - text.fromDictionary(dictionary: dictionary) // fill in the fields inherited from Node if let fontDict = dictionary["font"] as? [String:Any] { text.font = Font(dictionary: fontDict) @@ -81,18 +76,22 @@ class NodeSerializer { let contents = dictionary["contents"] as! [[String:Any]] var nodes = [Node]() for dict in contents { - nodes.append(NodeSerializer.shared.instance(dictionary: dict)) + nodes.append(self.instance(dictionary: dict)) } - let group = Group() - group.fromDictionary(dictionary: dictionary) // fill in the fields inherited from Node - group.contents = nodes - return group + return Group(contents: nodes) } } func instance(dictionary: [String:Any]) -> Node { let type = dictionary["node"] as! String - return factories[type]!(dictionary) + let node = factories[type]!(dictionary) + node.place = Transform(string: dictionary["place"] as? String) + node.opaque = Bool(dictionary["opaque"] as? String ?? "") ?? true + node.opacity = Double(dictionary["opacity"] as? String ?? "") ?? 0 + if let locusDict = dictionary["clip"] as? [String:Any] { + node.clip = locusSerializer.instance(dictionary: locusDict) + } + return node } } @@ -105,15 +104,6 @@ extension Node { } return result } - - func fromDictionary(dictionary: [String:Any]) { - place = Transform(string: dictionary["place"] as? String) - opaque = Bool(dictionary["opaque"] as? String ?? "") ?? true - opacity = Double(dictionary["opacity"] as? String ?? "") ?? 0 - if let locusDict = dictionary["clip"] as? [String:Any] { - clip = LocusSerializer.shared.instance(dictionary: locusDict) - } - } } extension Shape: Serializable { @@ -171,14 +161,10 @@ extension Group: Serializable { } } - - class LocusSerializer { var factories = [String:([String:Any]) -> Locus]() - static var shared = LocusSerializer() - init() { factories["Arc"] = { dictionary in Arc(ellipse: self.instance(dictionary: dictionary["Ellipse"] as! [String:Any]) as! Ellipse, @@ -206,7 +192,9 @@ class LocusSerializer { let array = dictionary["segments"] as! [[String:Any]] var pathSegments = [PathSegment]() for dict in array { - pathSegments.append(PathSegment(dictionary: dict)) + pathSegments.append(PathSegment( + type: typeForString(dict["type"] as! String), + data: dict["data"] as! [Double])) } return Path(segments: pathSegments) } @@ -228,7 +216,7 @@ class LocusSerializer { ry: parse(dictionary["ry"])) } } - + func instance(dictionary: [String:Any]) -> Locus { let type = dictionary["type"] as! String return factories[type]!(dictionary) @@ -302,20 +290,11 @@ extension RoundRect: Serializable { } } - - extension PathSegment: Serializable { - internal func toDictionary() -> [String:Any] { + func toDictionary() -> [String:Any] { return ["type": "\(type)", "data": data] } - - convenience init(dictionary: [String:Any]) { - guard let typeString = dictionary["type"] as? String, let array = dictionary["data"] as? [Double] else { self.init(); return } - - self.init(type: typeForString(typeString), - data: array) - } } extension Color: Serializable { @@ -331,7 +310,7 @@ extension Color: Serializable { extension Stroke: Serializable { - internal func toDictionary() -> [String:Any] { + func toDictionary() -> [String:Any] { var result = ["width": width, "cap": "\(cap)", "join": "\(join)", "dashes": dashes] as [String : Any] if let fillColor = fill as? Color { result["fill"] = fillColor.toDictionary() @@ -339,7 +318,7 @@ extension Stroke: Serializable { return result } - internal convenience init?(dictionary: [String:Any]) { + convenience init?(dictionary: [String:Any]) { guard let fillDict = dictionary["fill"] as? [String:Any], let fillType = fillDict["type"] as? String, fillType == "Color", let fill = Color(dictionary: fillDict) else { return nil @@ -363,7 +342,7 @@ extension Stroke: Serializable { extension Font: Serializable { - internal func toDictionary() -> [String:Any] { + func toDictionary() -> [String:Any] { return ["name": name, "size": size, "weight": weight] } @@ -407,7 +386,7 @@ extension Transform { extension Align { - internal func toString() -> String { + func toString() -> String { if self === Align.mid { return "mid" } @@ -417,7 +396,7 @@ extension Align { return "min" } - internal static func instantiate(string: String) -> Align { + static func instantiate(string: String) -> Align { switch string { case "mid": return .mid @@ -429,8 +408,6 @@ extension Align { } } - - fileprivate func typeForString(_ string: String) -> PathSegmentType { switch(string) { case "M": return .M From 9d6e0f6d41578a2a8cc6f15bb2845fca1500a051 Mon Sep 17 00:00:00 2001 From: Alisa Mylnikova Date: Sat, 28 Apr 2018 11:59:17 +0700 Subject: [PATCH 5/9] Fix wrong text-anchor parsing --- Source/svg/SVGParser.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/svg/SVGParser.swift b/Source/svg/SVGParser.swift index f8a4f1c3..1920fddc 100644 --- a/Source/svg/SVGParser.swift +++ b/Source/svg/SVGParser.swift @@ -843,7 +843,7 @@ open class SVGParser { if let anchor = textAnchor { if anchor == "middle" { return .mid - } else if anchor == "right" { + } else if anchor == "end" { return .max } } From e95a56a178c3b62db05d4625b8873150cee93ab6 Mon Sep 17 00:00:00 2001 From: Alisa Mylnikova Date: Sat, 28 Apr 2018 12:50:40 +0700 Subject: [PATCH 6/9] More MacOS adaptation --- Source/platform/iOS/Common_iOS.swift | 4 ++++ Source/platform/macOS/Common_macOS.swift | 4 ++++ Source/render/RenderUtils.swift | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Source/platform/iOS/Common_iOS.swift b/Source/platform/iOS/Common_iOS.swift index 349f5ed7..4f418535 100644 --- a/Source/platform/iOS/Common_iOS.swift +++ b/Source/platform/iOS/Common_iOS.swift @@ -77,6 +77,10 @@ extension MFont { class var mSystemFontSize: CGFloat { return UIFont.systemFontSize } + + class var mFamilyNames: [String] { + return UIFont.familyNames + } } extension UIScreen { diff --git a/Source/platform/macOS/Common_macOS.swift b/Source/platform/macOS/Common_macOS.swift index 7ac0eb45..638e38a6 100644 --- a/Source/platform/macOS/Common_macOS.swift +++ b/Source/platform/macOS/Common_macOS.swift @@ -92,6 +92,10 @@ extension NSFont { class var mSystemFontSize: CGFloat { return NSFont.systemFontSize } + + class var mFamilyNames: [String] { + return NSFontManager.shared.availableFontFamilies + } } extension NSScreen { diff --git a/Source/render/RenderUtils.swift b/Source/render/RenderUtils.swift index 556ed68e..4549c13e 100644 --- a/Source/render/RenderUtils.swift +++ b/Source/render/RenderUtils.swift @@ -90,7 +90,7 @@ class RenderUtils { fatalError("Unsupported node: \(node)") } - static let availableFonts = MFont.familyNames.map{ $0.lowercased() } + static let availableFonts = MFont.mFamilyNames.map{ $0.lowercased() } class func loadFont(name: String, size: Int) -> MFont? { From 62da85fad6da49d5ad04047726acbc85ea7e8aa1 Mon Sep 17 00:00:00 2001 From: Alisa Mylnikova Date: Sat, 28 Apr 2018 13:29:46 +0700 Subject: [PATCH 7/9] Fix stroke-width scientific notation --- Source/svg/SVGParser.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Source/svg/SVGParser.swift b/Source/svg/SVGParser.swift index f8a4f1c3..7ba0950c 100644 --- a/Source/svg/SVGParser.swift +++ b/Source/svg/SVGParser.swift @@ -636,13 +636,8 @@ open class SVGParser { } fileprivate func getStrokeWidth(_ styleParts: [String: String]) -> Double { - if let strokeWidth = styleParts["stroke-width"] { - let characterSet = NSCharacterSet.decimalDigits.union(NSCharacterSet.punctuationCharacters).inverted - let digitsArray = strokeWidth.components(separatedBy: characterSet) - let digits = digitsArray.joined() - if let value = Double(digits) { - return value - } + if let strokeWidth = styleParts["stroke-width"], let value = doubleFromString(strokeWidth) { + return value } return 1 } From 4f129bc0c5ebf7a8321e92aa21445303b60a2423 Mon Sep 17 00:00:00 2001 From: Yuri Strot Date: Thu, 3 May 2018 16:43:14 +0700 Subject: [PATCH 8/9] Add W3C Test Suite coverage page --- MacawTests/w3c-test-suite.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 MacawTests/w3c-test-suite.md diff --git a/MacawTests/w3c-test-suite.md b/MacawTests/w3c-test-suite.md new file mode 100644 index 00000000..2811ec3a --- /dev/null +++ b/MacawTests/w3c-test-suite.md @@ -0,0 +1,14 @@ +## W3C SVG Test Suite Coverage + +Total: 6 + +Passed: 33% + +|Name |Status | +|------|-------| +|[color-prop-01-b-manual](w3cSVGTests/color-prop-01-b-manual.svg) | ❌ | +|[color-prop-02-f-manual](w3cSVGTests/color-prop-02-f-manual.svg) | ✅ | +|[color-prop-03-t-manual](w3cSVGTests/color-prop-03-t-manual.svg) | [#123](https://github.com/exyte/Macaw/issues/123) | +|[color-prop-04-t-manual](w3cSVGTests/color-prop-04-t-manual.svg) | [#42](https://github.com/exyte/Macaw/issues/42) | +|[color-prop-05-t-manual](w3cSVGTests/color-prop-05-t-manual.svg) | ❌ | +|[shapes-circle-01-t-manual](w3cSVGTests/shapes-circle-01-t-manual.svg) | ✅ | From d749c33e789af8f0f91fb2562d68b02a87609f78 Mon Sep 17 00:00:00 2001 From: Alisa Mylnikova Date: Thu, 3 May 2018 17:28:20 +0700 Subject: [PATCH 9/9] Logic fix --- Source/render/RenderUtils.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/render/RenderUtils.swift b/Source/render/RenderUtils.swift index 4549c13e..4f3462e8 100644 --- a/Source/render/RenderUtils.swift +++ b/Source/render/RenderUtils.swift @@ -100,13 +100,13 @@ class RenderUtils { return MFont(name: font, size: CGFloat(size)) } - if name == "serif" { + if font == "serif" { return MFont(name: "Georgia", size: CGFloat(size)) } - if name == "sans-serif" { + if font == "sans-serif" { return MFont(name: "Arial", size: CGFloat(size)) } - if name == "monospace" { + if font == "monospace" { return MFont(name: "Courier", size: CGFloat(size)) } }