1
1
mirror of https://github.com/exyte/Macaw.git synced 2024-08-16 08:30:33 +03:00

Merge pull request #163 from exyte/feature/add-macaw-to-svg-serializer

Aadd Macaw to SVG serializer
This commit is contained in:
Victor Sukochev 2017-09-14 11:56:58 +07:00 committed by GitHub
commit dd3d1a72b7
6 changed files with 301 additions and 1 deletions

View File

@ -144,6 +144,8 @@
A718CD501F45C28F00966E06 /* MView_macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A718CD4C1F45C28F00966E06 /* MView_macOS.swift */; };
A718CD521F45C2A400966E06 /* MBezierPath+Extension_macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A718CD511F45C2A400966E06 /* MBezierPath+Extension_macOS.swift */; };
A7E675561EC4213500BD9ECB /* NodeBoundsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E675551EC4213500BD9ECB /* NodeBoundsTests.swift */; };
C4820B181F458D0E008CE0FF /* SVGSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4820B171F458D0E008CE0FF /* SVGSerializer.swift */; };
C4820B1A1F458D64008CE0FF /* MacawSVGTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4820B191F458D64008CE0FF /* MacawSVGTests.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -309,6 +311,8 @@
A718CD4C1F45C28F00966E06 /* MView_macOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MView_macOS.swift; path = Source/platform/macOS/MView_macOS.swift; sourceTree = SOURCE_ROOT; };
A718CD511F45C2A400966E06 /* MBezierPath+Extension_macOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "MBezierPath+Extension_macOS.swift"; path = "Source/platform/macOS/MBezierPath+Extension_macOS.swift"; sourceTree = SOURCE_ROOT; };
A7E675551EC4213500BD9ECB /* NodeBoundsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NodeBoundsTests.swift; path = Bounds/NodeBoundsTests.swift; sourceTree = "<group>"; };
C4820B171F458D0E008CE0FF /* SVGSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGSerializer.swift; sourceTree = "<group>"; };
C4820B191F458D64008CE0FF /* MacawSVGTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MacawSVGTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -598,6 +602,7 @@
57E5E1481E3B393900D1CB28 /* SVGParserError.swift */,
57E5E1491E3B393900D1CB28 /* SVGParserRegexHelper.swift */,
57E5E14A1E3B393900D1CB28 /* SVGView.swift */,
C4820B171F458D0E008CE0FF /* SVGSerializer.swift */,
);
path = svg;
sourceTree = "<group>";
@ -660,6 +665,7 @@
57CAB1241D7832E000FD8E47 /* svg */,
57CAB1221D782DFC00FD8E47 /* TestUtils.swift */,
57FCD27B1D76EA4600CC0FB6 /* MacawTests.swift */,
C4820B191F458D64008CE0FF /* MacawSVGTests.swift */,
57FCD27D1D76EA4600CC0FB6 /* Info.plist */,
);
path = MacawTests;
@ -870,6 +876,7 @@
57E5E1951E3B393900D1CB28 /* PathSegment.swift in Sources */,
57E5E1A41E3B393900D1CB28 /* ImageRenderer.swift in Sources */,
57E5E1621E3B393900D1CB28 /* PathFunctions.swift in Sources */,
C4820B181F458D0E008CE0FF /* SVGSerializer.swift in Sources */,
57E5E16E1E3B393900D1CB28 /* MorphingAnimation.swift in Sources */,
57A27BD11E44C5460057BD3A /* ShapeAnimation.swift in Sources */,
57E5E15F1E3B393900D1CB28 /* TransformInterpolation.swift in Sources */,
@ -956,6 +963,7 @@
files = (
5713C4F71E5C34C700BBA4D9 /* SequenceAnimationTests.swift in Sources */,
57B7A4E31EE70DC3009D78D7 /* ImageBoundsTests.swift in Sources */,
C4820B1A1F458D64008CE0FF /* MacawSVGTests.swift in Sources */,
5713C4F91E5C3FEE00BBA4D9 /* DelayedAnimationTests.swift in Sources */,
5713C4F31E5AD46800BBA4D9 /* ControlStatesTests.swift in Sources */,
57FCD27C1D76EA4600CC0FB6 /* MacawTests.swift in Sources */,

View File

@ -0,0 +1,27 @@
import XCTest
@testable import Macaw
class MacawSVGTests: XCTestCase {
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
super.setUp()
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testSVGEllipse() {
let bundle = Bundle(for: type(of: TestUtils()))
let ellipseReferenceContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" ><g><ellipse cy=\"80\" ry=\"50\" rx=\"100\" cx=\"200\" fill=\"yellow\" stroke=\"purple\" stroke-width=\"2\"/></g></svg>"
do {
let node = try SVGParser.parse(bundle:bundle, path: "ellipse")
XCTAssert(SVGSerializer.serialize(node: node) == ellipseReferenceContent)
} catch {
print(error)
}
}
}

View File

@ -0,0 +1 @@
ellipse

View File

@ -293,6 +293,13 @@ open class SVGConstants {
"yellowgreen": 0x9acd32
]
open static func valueToColor(_ color: Int) -> String? {
return SVGConstants.colorList.filter { (k, v) -> Bool in v == color }.map { (k, v) -> String in k }.first
}
open static let defaultStrokeLineCap = LineCap.butt
open static let defaultStrokeLineJoin = LineJoin.miter
open static let moveToAbsolute = "M"
open static let moveToRelative = "m"
open static let lineToAbsolute = "L"

View File

@ -449,7 +449,7 @@ open class SVGParser {
}
fileprivate func getStrokeCap(_ styleParts: [String: String]) -> LineCap {
var cap = LineCap.square
var cap = LineCap.butt
if let strokeCap = styleParts["stroke-linecap"] {
switch strokeCap {
case "butt":

View File

@ -0,0 +1,257 @@
//
// SVGSerializer.swift
// Macaw
//
// Created by Yuriy Kashnikov on 8/17/17.
// Copyright © 2017 Exyte. All rights reserved.
//
import Foundation
///
/// This class serializes Macaw Scene into an SVG String
///
open class SVGSerializer {
fileprivate let width: Int?
fileprivate let height: Int?
fileprivate let id: String?
fileprivate let indent: Int
fileprivate init(width: Int?, height: Int?, id: String?) {
self.width = width
self.height = height
self.id = id
self.indent = 0
}
// header and footer
fileprivate let SVGDefaultHeader = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\""
fileprivate static let SVGDefaultId = ""
fileprivate static let SVGUndefinedSize = -1
fileprivate let SVGFooter = "</svg>"
// groups
fileprivate let SVGGroupOpenTag = "<g"
fileprivate let SVGGroupCloseTag = "</g>"
// shapes
fileprivate let SVGRectOpenTag = "<rect "
fileprivate let SVGCircleOpenTag = "<circle "
fileprivate let SVGEllipseOpenTag = "<ellipse "
fileprivate let SVGLineOpenTag = "<line "
fileprivate let SVGPolylineOpenTag = "<polyline "
fileprivate let SVGPolygonOpenTag = "<polygon "
fileprivate let SVGPathOpenTag = "<path "
fileprivate let SVGImageOpenTag = "<image "
fileprivate let SVGTextOpenTag = "<text "
fileprivate let SVGGenericEndTag = ">"
fileprivate let SVGGenericCloseTag = "/>"
fileprivate let SVGUndefinedTag = "<UNDEFINED "
fileprivate let indentPrefixSymbol = " "
fileprivate func indentTextWithOffset(_ text: String, _ offset: Int) -> String {
if self.indent != 0 {
let prefix = String(repeating: indentPrefixSymbol, count:self.indent)
return "\n\(String(repeating: prefix, count:offset))\(text)"
}
return text
}
fileprivate func att(_ a: Double) -> String {
return String(Int(a))
}
fileprivate func tag(_ tag: String, _ args: [String:String], close: Bool=false) -> String {
let attrs = args.map { "\($0)=\"\($1)\"" }.joined(separator: " ")
let closeTag = close ? " />" : ""
return "\(tag) \(attrs) \(closeTag)"
}
fileprivate func arcToSVG(_ arc: Macaw.Arc) -> String {
if (arc.shift == 0.0) {
return tag(SVGEllipseOpenTag, ["cx":att(arc.ellipse.cx), "cy":att(arc.ellipse.cy), "rx":att(arc.ellipse.rx), "ry":att(arc.ellipse.ry)])
} else {
// Convert arc to SVG format with x axis rotation, arc flag, and sweep flag
return "\(SVGUndefinedTag) arc is not implemented yet"
}
}
fileprivate func polygonToSVG(_ polygon: Macaw.Polygon) -> String {
let points = polygon.points.flatMap { String($0) }.joined(separator: ",")
return tag(SVGPolygonOpenTag, ["points":points])
}
fileprivate func polylineToSVG(_ polyline: Macaw.Polyline) -> String {
let points = polyline.points.flatMap { String($0) }.joined(separator: ",")
return tag(SVGPolylineOpenTag, ["points":points])
}
fileprivate func pathToSVG(_ path: Macaw.Path) -> String {
var d = ""
for segment in path.segments {
d += "\(segment.type) \(segment.data.flatMap { String(Int($0)) }.joined(separator: " "))"
}
return tag(SVGPathOpenTag, ["d":d])
}
fileprivate func lineToSVG(_ line: Macaw.Line) -> String {
return tag(SVGLineOpenTag, ["x1":String(Int(line.x1)), "y1":att(line.y1), "x2":att(line.x2), "y2":att(line.y2)])
}
fileprivate func ellipseToSVG(_ ellipse: Macaw.Ellipse) -> String {
return tag(SVGEllipseOpenTag, ["cx":att(ellipse.cx), "cy":att(ellipse.cy), "rx":att(ellipse.rx), "ry":att(ellipse.ry)])
}
fileprivate func circleToSVG(_ circle: Macaw.Circle) -> String {
return tag(SVGCircleOpenTag, ["cx":att(circle.cx), "cy":att(circle.cy), "r":att(circle.r)])
}
fileprivate func roundRectToSVG(_ roundRect: Macaw.RoundRect) -> String {
return tag(SVGRectOpenTag, ["rx":att(roundRect.rx), "ry":att(roundRect.ry), "width":att(roundRect.rect.w), "height":att(roundRect.rect.h)])
}
fileprivate func rectToSVG(_ rect: Macaw.Rect) -> String {
return tag(SVGRectOpenTag, ["x":att(rect.x), "y":att(rect.y), "width":att(rect.w), "height":att(rect.h)])
}
fileprivate func imageToSVG(_ image: Macaw.Image) -> String {
return tag(SVGImageOpenTag, ["xlink:href":image.src, "x":att(image.place.dx), "y":att(image.place.dy), "width":String(image.w), "height":String(image.h)], close: true)
}
fileprivate func textToSVG(_ text: Macaw.Text) -> String {
var result = tag(SVGTextOpenTag, ["x":att(text.place.dx), "y":att(text.place.dy)])
if let font = text.font {
result += "font-family=\"\(font.name)\" font-size=\"\(font.size)\""
}
result += SVGGenericEndTag
result += text.text
result += "</text>"
return result
}
fileprivate func fillToSVG(_ shape: Shape) -> String {
if let fillColor = shape.fillVar.value as? Color {
if let fill = SVGConstants.valueToColor(fillColor.val) {
return " fill=\"\(fill)\""
} else {
return " fill=\"#\(String(format:"%6X", fillColor.val))\""
}
}
return " fill=\"none\""
}
fileprivate func strokeToSVG(_ shape: Shape) -> String {
var result = ""
if let strokeColor = shape.strokeVar.value?.fill as? Color {
if let stroke = SVGConstants.valueToColor(strokeColor.val) {
result += " stroke=\"\(stroke)\""
} else {
result += " stroke=\"#\(String(format:"%6X", strokeColor.val))\""
}
}
if let strokeWidth = shape.strokeVar.value?.width {
result += " stroke-width=\"\(Int(strokeWidth))\""
}
if let strokeCap = shape.strokeVar.value?.cap {
if strokeCap != SVGConstants.defaultStrokeLineCap {
result += " stroke-linecap=\"\(strokeCap)\""
}
}
if let strokeJoin = shape.strokeVar.value?.join {
if strokeJoin != SVGConstants.defaultStrokeLineJoin {
result += " stroke-linejoin=\"\(strokeJoin)\""
}
}
return result
}
fileprivate func macawShapeToSvgShape (macawShape: Shape) -> String {
var result = ""
let locus = macawShape.formVar.value
switch locus {
case let arc as Macaw.Arc:
result += arcToSVG(arc)
case let polygon as Macaw.Polygon:
result += polygonToSVG(polygon)
case let polyline as Macaw.Polyline:
result += polylineToSVG(polyline)
case let path as Macaw.Path:
result += pathToSVG(path)
case let line as Macaw.Line:
result += lineToSVG(line)
case let ellipse as Macaw.Ellipse:
result += ellipseToSVG(ellipse)
case let circle as Macaw.Circle:
result += circleToSVG(circle)
case let roundRect as Macaw.RoundRect:
result += roundRectToSVG(roundRect)
case let rect as Macaw.Rect:
result += rectToSVG(rect)
default:
result += "\(SVGUndefinedTag) locus:\(locus)"
}
result += fillToSVG(macawShape)
result += strokeToSVG(macawShape)
result += SVGGenericCloseTag
return result
}
fileprivate func serialize(node: Node, offset: Int) -> String {
if let shape = node as? Shape {
return indentTextWithOffset(macawShapeToSvgShape(macawShape: shape), offset)
}
if let group = node as? Group {
var result = indentTextWithOffset(SVGGroupOpenTag, offset)
if ([group.place.dx, group.place.dy].map{ Int($0) } != [0, 0]) {
if ([group.place.m11, group.place.m12, group.place.m21, group.place.m22].map { Int($0) } == [1, 0, 0, 1]) {
result += " transform=\"translate(\(Int(group.place.dx)),\(Int(group.place.dy)))\""
} else {
let matrixArgs = [group.place.m11, group.place.m12, group.place.m21, group.place.m22, group.place.dx, group.place.dy].map{ String($0) }.joined(separator: ",")
result += " transform=\"matrix(\(matrixArgs))\""
}
}
result += SVGGenericEndTag
for child in group.contentsVar.value {
result += serialize(node: child, offset: offset + 1)
}
result += indentTextWithOffset(SVGGroupCloseTag, offset)
return result
}
if let image = node as? Image {
return imageToSVG(image)
}
if let text = node as? Macaw.Text {
return textToSVG(text)
}
return "SVGUndefinedTag \(node)"
}
fileprivate func serialize(node:Node) -> String {
var optionalSection = ""
if let w = width {
optionalSection += "width=\"\(w)\""
}
if let h = height {
optionalSection += "height=\"\(h)\""
}
if let i = id {
optionalSection += "id=\"\(i)\""
}
var result = [SVGDefaultHeader, optionalSection, SVGGenericEndTag].joined(separator: " ")
result += serialize(node: node, offset: 1)
result += indentTextWithOffset(SVGFooter, 0)
return result
}
open class func serialize(node: Node, width: Int? = nil, height: Int? = nil, id: String? = nil) -> String {
return SVGSerializer(width: width, height:height, id: id).serialize(node: node)
}
}