// // SceneSerialization.swift // MacawTests // // Created by Alisa Mylnikova on 23/04/2018. // Copyright © 2018 Exyte. All rights reserved. // #if os(OSX) import Foundation @testable import MacawOSX #endif #if os(iOS) import UIKit @testable import Macaw #endif protocol Initializable { init() } extension Double : Initializable {} extension Bool : Initializable {} extension Int : Initializable {} func parse(_ from: Any?) -> T { return from as? T ?? T() } protocol Serializable { func toDictionary() -> [String:Any] } class NodeSerializer { var factories = [String:([String:Any]) -> Node]() let locusSerializer = LocusSerializer() init() { factories["Shape"] = { dictionary in let locusDict = dictionary["form"] as! [String:Any] let locus = self.locusSerializer.instance(dictionary: locusDict) let shape = Shape(form: locus) if let fillDict = dictionary["fill"] as? [String:Any], let fillType = fillDict["type"] as? String, fillType == "Color" { shape.fill = Color(dictionary: fillDict) } if let strokeDict = dictionary["stroke"] as? [String:Any] { shape.stroke = Stroke(dictionary: strokeDict) } return shape } factories["Text"] = { dictionary in let textString = dictionary["text"] as! String let text = Text(text: textString) if let fontDict = dictionary["font"] as? [String:Any] { text.font = Font(dictionary: fontDict) } if let fillDict = dictionary["fill"] as? [String:Any], let fillType = fillDict["type"] as? String, fillType == "Color", let color = Color(dictionary: fillDict) { text.fill = color } if let strokeDict = dictionary["stroke"] as? [String:Any] { text.stroke = Stroke(dictionary: strokeDict) } if let alignString = dictionary["align"] as? String { text.align = Align.instantiate(string: alignString) } if let baselineString = dictionary["baseline"] as? String { text.baseline = baselineForString(baselineString) } return text } factories["Group"] = { dictionary in let contents = dictionary["contents"] as! [[String:Any]] var nodes = [Node]() for dict in contents { nodes.append(self.instance(dictionary: dict)) } return Group(contents: nodes) } factories["Canvas"] = { dictionary in let layoutDict = dictionary["layout"] as! [String : Any] let viewBoxDict = layoutDict["viewBox"] as! [String:Any] let layout = SVGNodeLayout( svgSize: SVGSize(dictionary: layoutDict["svgSize"] as! [String : Any]), viewBox: self.locusSerializer.instance(dictionary: viewBoxDict) as? Rect, scaling: AspectRatio.instantiate(string: layoutDict["scalingMode"] as! String), xAlign: Align.instantiate(string: layoutDict["xAligningMode"] as! String), yAlign: Align.instantiate(string: layoutDict["yAligningMode"] as! String)) let contents = dictionary["contents"] as! [[String:Any]] var nodes = [Node]() for dict in contents { nodes.append(self.instance(dictionary: dict)) } return SVGCanvas(layout: layout, contents: nodes) } } func instance(dictionary: [String:Any]) -> Node { let type = dictionary["node"] as! String 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 } } extension Node { func baseToDictionary() -> [String:Any] { var result = [String : Any]() if place != .identity { result["place"] = place.toString() } if !opaque { result["opaque"] = String(describing: opaque) result["opacity"] = String(describing: opacity) } if let clip = clip as? Serializable { result["clip"] = clip.toDictionary() } if let mask = mask as? Serializable { result["mask"] = mask.toDictionary() } return result } } extension Shape: Serializable { func toDictionary() -> [String:Any] { var result = super.baseToDictionary() result["node"] = "Shape" if let form = form as? Serializable { result["form"] = form.toDictionary() } if let fillColor = fill as? Color { result["fill"] = fillColor.toDictionary() } if let stroke = stroke { result["stroke"] = stroke.toDictionary() } return result } } extension Text: Serializable { func toDictionary() -> [String:Any] { var result = super.baseToDictionary() result["node"] = "Text" result["text"] = text if let font = font { result["font"] = font.toDictionary() } if let fillColor = fill as? Color { result["fill"] = fillColor.toDictionary() } if let stroke = stroke { result["stroke"] = stroke.toDictionary() } result["align"] = align.toString() result["baseline"] = "\(baseline)" return result } } extension Group: Serializable { func toDictionary() -> [String:Any] { if let canvas = self as? SVGCanvas { return canvas.canvasDictionary() } var nodes = [[String:Any]]() for node in contents { if let node = node as? Serializable { nodes.append(node.toDictionary()) } } var result = super.baseToDictionary() result["node"] = "Group" result["contents"] = nodes return result } } extension SVGCanvas { func canvasDictionary() -> [String:Any] { var nodes = [[String:Any]]() for node in contents { if let node = node as? Serializable { nodes.append(node.toDictionary()) } } var result = super.baseToDictionary() result["node"] = "Canvas" result["layout"] = (layout as! SVGNodeLayout).toDictionary() result["contents"] = nodes return result } } extension SVGNodeLayout: Serializable { func toDictionary() -> [String:Any] { return ["svgSize" : svgSize.toDictionary() as Any, "viewBox" : viewBox?.toDictionary() as Any, "scalingMode" : scaling.toString(), "xAligningMode" : xAlign.toString(), "yAligningMode" : yAlign.toString()] } } extension SVGSize: Serializable { func toDictionary() -> [String:Any] { return ["width" : width.toString(), "height": height.toString()] } convenience init(dictionary: [String:Any]) { self.init(width: SVGLength(string: dictionary["width"] as? String), height: SVGLength(string: dictionary["height"] as? String)) } } extension SVGLength { func toString() -> String { switch(self) { case let .percent(percent): return "\(percent)%" case let .pixels(pixels): return String(describing: pixels) } } init(string: String?) { self.init(pixels: 0) guard let string = string else { return } if string.hasSuffix("%") { self = SVGLength.percent(Double(string.dropLast())!) } else { self = SVGLength.pixels(Double(string)!) } } } class LocusSerializer { var factories = [String:([String:Any]) -> Locus]() init() { factories["Arc"] = { dictionary in Arc(ellipse: self.instance(dictionary: dictionary["Ellipse"] as! [String:Any]) as! Ellipse, shift: parse(dictionary["shift"]), extent: parse(dictionary["extent"])) } factories["Circle"] = { dictionary in Circle(cx: parse(dictionary["cx"]), cy: parse(dictionary["cy"]), r: parse(dictionary["r"])) } factories["Ellipse"] = { dictionary in Ellipse(cx: parse(dictionary["cx"]), cy: parse(dictionary["cy"]), rx: parse(dictionary["rx"]), ry: parse(dictionary["rx"])) } factories["Line"] = { dictionary in Line(x1: parse(dictionary["x1"]), y1: parse(dictionary["y1"]), x2: parse(dictionary["x2"]), y2: parse(dictionary["y2"])) } factories["Path"] = { dictionary in let array = dictionary["segments"] as! [[String:Any]] var pathSegments = [PathSegment]() for dict in array { pathSegments.append(PathSegment( type: typeForString(dict["type"] as! String), data: dict["data"] as! [Double])) } return Path(segments: pathSegments) } factories["Polygon"] = { dictionary in Polygon.init(points: dictionary["points"] as! [Double]) } factories["Polyline"] = { dictionary in Polyline.init(points: dictionary["points"] as! [Double]) } factories["Rect"] = { dictionary in Rect(x: parse(dictionary["x"]), y: parse(dictionary["y"]), w: parse(dictionary["w"]), h: parse(dictionary["h"])) } factories["RoundRect"] = { dictionary in RoundRect(rect: self.instance(dictionary: dictionary["Rect"] as! [String:Any]) as! Rect, rx: parse(dictionary["rx"]), ry: parse(dictionary["ry"])) } factories["TransformedLocus"] = { dictionary in TransformedLocus(locus: self.instance(dictionary: dictionary["locus"] as! [String:Any]), transform: Transform(string: dictionary["transform"] as? String)) } } func instance(dictionary: [String:Any]) -> Locus { let type = dictionary["type"] as! String return factories[type]!(dictionary) } } extension TransformedLocus: Serializable { func toDictionary() -> [String:Any] { return ["type": "TransformedLocus", "locus": (locus as! Serializable).toDictionary(), "transform": transform.toString()] } } extension Arc: Serializable { func toDictionary() -> [String:Any] { return ["type": "Arc", "ellipse": ellipse.toDictionary(), "shift": shift, "extent": extent] } } extension Circle: Serializable { func toDictionary() -> [String:Any] { return ["type": "Circle", "cx": cx, "cy": cy, "r": r] } } extension Ellipse: Serializable { func toDictionary() -> [String:Any] { return ["type": "Ellipse", "cx": cx, "cy": cy, "rx": rx, "ry": ry] } } extension Line: Serializable { func toDictionary() -> [String:Any] { return ["type": "Line", "x1": x1, "y1": y1, "x2": x2, "y2": y2] } } extension Path: Serializable { func toDictionary() -> [String:Any] { var pathSegments = [[String:Any]]() for segment in segments { pathSegments.append(segment.toDictionary()) } return ["type": "Path", "segments": pathSegments] } } #if os(iOS) extension Polygon: Serializable { func toDictionary() -> [String:Any] { return ["type": "Polygon", "points": points] } } #endif #if os(OSX) extension MacawOSX.Polygon: Serializable { func toDictionary() -> [String:Any] { return ["type": "Polygon", "points": points] } } #endif extension Polyline: Serializable { func toDictionary() -> [String:Any] { return ["type": "Polyline", "points": points] } } extension Rect: Serializable { func toDictionary() -> [String:Any] { return ["type": "Rect", "x": x, "y": y, "w": w, "h": h] } } extension RoundRect: Serializable { func toDictionary() -> [String:Any] { return ["type": "RoundRect", "rect": rect.toDictionary(), "rx": rx, "ry": ry] } } extension PathSegment: Serializable { func toDictionary() -> [String:Any] { return ["type": "\(type)", "data": data] } } extension Color: Serializable { func toDictionary() -> [String:Any] { return ["type": "Color", "val": val] } convenience init?(dictionary: [String:Any]) { self.init(val: dictionary["val"] as! Int) } } extension Stroke: Serializable { 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() } if miterLimit != 4 { result["miterLimit"] = miterLimit } if offset != 0 { result["offset"] = offset } return result } 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 } var cap = LineCap.butt if let lineCapString = dictionary["cap"] as? String { cap = lineCapForString(lineCapString) } var join = LineJoin.miter if let lineJoinString = dictionary["join"] as? String { join = lineJoinForString(lineJoinString) } var miterLimit: Double? if let string = dictionary["miterLimit"] as? String { miterLimit = Double(string) } var offset: Double? if let string = dictionary["offset"] as? String { offset = Double(string) } let dashes = dictionary["dashes"] as? [Double] ?? [] self.init(fill: fill, width: parse(dictionary["width"]), cap: cap, join: join, miterLimit: miterLimit != nil ? miterLimit! : 4, dashes: dashes, offset: offset != nil ? offset! : 0) } } extension Font: Serializable { func toDictionary() -> [String:Any] { return ["name": name, "size": size, "weight": weight] } convenience init(dictionary: [String:Any]) { self.init(name: dictionary["name"] as? String ?? "Serif", size: parse(dictionary["size"]), weight: dictionary["weight"] as? String ?? "normal") } } extension Transform { func toString() -> String { let formatter = NumberFormatter() formatter.minimumFractionDigits = 0 formatter.maximumFractionDigits = 6 let nums = [m11, m12, m21, m22, dx, dy] var result = "" for num in nums { result += formatter.string(from: num as NSNumber) ?? "n/a" result += ", " } return result.dropLast(2) + "" } convenience init(string: String?) { guard let string = string else { self.init() return } let vals = string.components(separatedBy: ", ").map{ Double($0) ?? 0 } if vals.count == 6 { self.init(m11: vals[0], m12: vals[1], m21: vals[2], m22: vals[3], dx: vals[4], dy: vals[5]) } else { self.init() } } } extension Align { func toString() -> String { if self === Align.mid { return "mid" } if self === Align.max { return "max" } return "min" } static func instantiate(string: String) -> Align { switch string { case "mid": return .mid case "max": return .max default: return .min } } } extension AspectRatio { func toString() -> String { if self === AspectRatio.meet { return "meet" } if self === AspectRatio.slice { return "slice" } return "none" } static func instantiate(string: String) -> AspectRatio { switch string { case "meet": return .meet case "slice": return .slice default: return .none } } } fileprivate func typeForString(_ string: String) -> PathSegmentType { switch(string) { case "M": return .M case "m": return .m case "L": return .L case "l": return .l case "C": return .C case "c": return .c case "Q": return .Q case "q": return .q case "A": return .A case "a": return .a case "z", "Z": return .z case "H": return .H case "h": return .h case "V": return .V case "v": return .v case "S": return .S case "s": return .s case "T": return .T case "t": return .t default: return .M } } fileprivate func lineCapForString(_ string: String) -> LineCap { switch(string) { case "butt": return .butt case "round": return .round case "square": return .square default: return .butt } } fileprivate func lineJoinForString(_ string: String) -> LineJoin { switch(string) { case "miter": return .miter case "round": return .round case "bevel": return .bevel default: return .miter } } fileprivate func baselineForString(_ string: String) -> Baseline { switch(string) { case "top": return .top case "alphabetic": return .alphabetic case "bottom": return .bottom case "mid": return .mid default: return .top } }