mirror of
https://github.com/exyte/Macaw.git
synced 2024-10-26 13:01:25 +03:00
Merge pull request #163 from exyte/feature/add-macaw-to-svg-serializer
Aadd Macaw to SVG serializer
This commit is contained in:
commit
dd3d1a72b7
@ -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 */,
|
||||
|
27
MacawTests/MacawSVGTests.swift
Normal file
27
MacawTests/MacawSVGTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
1
MacawTests/svg/svglist.txt
Normal file
1
MacawTests/svg/svglist.txt
Normal file
@ -0,0 +1 @@
|
||||
ellipse
|
@ -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"
|
||||
|
@ -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":
|
||||
|
257
Source/svg/SVGSerializer.swift
Normal file
257
Source/svg/SVGSerializer.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user