1
1
mirror of https://github.com/exyte/Macaw.git synced 2024-11-12 18:18:35 +03:00
Macaw/Source/svg/SVGSerializer.swift
2018-10-04 14:43:11 +07:00

414 lines
14 KiB
Swift

//
// 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 init(width: Int?, height: Int?, id: String?) {
self.width = width
self.height = height
self.id = id
}
// 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 SVGClipPathName = "clipPath"
fileprivate let SVGMaskName = "mask"
fileprivate let SVGEpsilon: Double = 0.00001
fileprivate let SVGDefaultOpacityValueAsAlpha = 1 * 255
fileprivate func tag(_ tag: String, _ args: [String: String]=[:], close: Bool = false) -> String {
let attrs = args.sorted { a1, a2 -> Bool in a1.key < a2.key }
.map { "\($0)=\"\($1)\"" }.joined(separator: " ")
let closeTag = close ? " />" : ""
return "\(tag) \(attrs) \(closeTag)"
}
fileprivate func arcToSVG(_ arc: Arc) -> String {
if arc.shift == 0.0 && abs(arc.extent - .pi * 2.0) < SVGEpsilon {
return tag(SVGEllipseOpenTag, ["cx": arc.ellipse.cx.serialize(), "cy": arc.ellipse.cy.serialize(), "rx": arc.ellipse.rx.serialize(), "ry": arc.ellipse.ry.serialize()])
} else {
let rx = arc.ellipse.rx
let ry = arc.ellipse.ry
let cx = arc.ellipse.cx
let cy = arc.ellipse.cy
let theta1 = arc.shift
let delta = arc.extent
let theta2 = theta1 + delta
let x1 = cx + rx * cos(theta1)
let y1 = cy + ry * sin(theta1)
let x2 = cx + rx * cos(theta2)
let y2 = cy + ry * sin(theta2)
let largeArcFlag = abs(delta) > .pi ? 1 : 0
let sweepFlag = delta > 0.0 ? 1 : 0
var d = "M\(x1),\(y1) "
d += "A \(rx),\(ry) 0.0 \(largeArcFlag), \(sweepFlag) \(x2),\(y2)"
return tag(SVGPathOpenTag, ["d": d])
}
}
fileprivate func polygonToSVG(_ polygon: Polygon) -> String {
let points = polygon.points.compactMap { String($0) }.joined(separator: ",")
return tag(SVGPolygonOpenTag, ["points": points])
}
fileprivate func polylineToSVG(_ polyline: Polyline) -> String {
let points = polyline.points.compactMap { String($0) }.joined(separator: ",")
return tag(SVGPolylineOpenTag, ["points": points])
}
fileprivate func pathToSVG(_ path: Path) -> String {
var d = ""
for segment in path.segments {
d += "\(segment.type) \(segment.data.compactMap { $0.serialize() }.joined(separator: " "))"
}
return tag(SVGPathOpenTag, ["d": d])
}
fileprivate func lineToSVG(_ line: Line) -> String {
return tag(SVGLineOpenTag, ["x1": line.x1.serialize(), "y1": line.y1.serialize(), "x2": line.x2.serialize(), "y2": line.y2.serialize()])
}
fileprivate func ellipseToSVG(_ ellipse: Ellipse) -> String {
return tag(SVGEllipseOpenTag, ["cx": ellipse.cx.serialize(), "cy": ellipse.cy.serialize(), "rx": ellipse.rx.serialize(), "ry": ellipse.ry.serialize()])
}
fileprivate func circleToSVG(_ circle: Circle) -> String {
return tag(SVGCircleOpenTag, ["cx": circle.cx.serialize(), "cy": circle.cy.serialize(), "r": circle.r.serialize()])
}
fileprivate func roundRectToSVG(_ roundRect: RoundRect) -> String {
return tag(SVGRectOpenTag, ["x": roundRect.rect.x.serialize(), "y": roundRect.rect.y.serialize(), "width": roundRect.rect.w.serialize(), "height": roundRect.rect.h.serialize(), "rx": roundRect.rx.serialize(), "ry": roundRect.ry.serialize()])
}
fileprivate func rectToSVG(_ rect: Rect) -> String {
return tag(SVGRectOpenTag, ["x": rect.x.serialize(), "y": rect.y.serialize(), "width": rect.w.serialize(), "height": rect.h.serialize()])
}
fileprivate func imageToSVG(_ image: Image) -> String {
var result = tag(SVGImageOpenTag, close: false)
result += idToSVG(image.tag)
result += clipToSVG(image.clip)
result += transformToSVG(image.place)
if image.src.contains("memory://") {
if let data = image.base64encoded(type: Image.ImageRepresentationType.PNG) {
result += " xlink:href=\"data:image/png;base64,\(data)\""
}
} else {
result += " xlink:href=\"\(image.src)\" "
}
if let bounds = image.bounds {
result += " width=\"\(String(bounds.w))\" height=\"\(String(bounds.h))\" "
}
result += SVGGenericCloseTag
return result
}
fileprivate func alignToSVG(_ align: Align) -> String {
if align === Align.mid {
return " text-anchor=\"middle\" "
}
if align === Align.max {
return " text-anchor=\"end "
}
return ""
}
fileprivate func baselineToSVG(_ baseline: Baseline) -> String {
if baseline == .top {
return " dominant-baseline=\"text-before-edge\" "
}
return ""
}
fileprivate func textToSVG(_ text: Text) -> String {
var result = tag(SVGTextOpenTag)
result += idToSVG(text.tag)
if let font = text.font {
result += " font-family=\"\(font.name)\" font-size=\"\(font.size)\" "
// TODO: check with enums
if font.name != "normal" {
result += " font-weight=\"\(font.weight)\" "
}
}
result += alignToSVG(text.align)
result += baselineToSVG(text.baseline)
result += clipToSVG(text.clip)
result += fillToSVG(text.fillVar.value)
result += strokeToSVG(text.strokeVar.value)
result += transformToSVG(text.place)
result += SVGGenericEndTag
result += text.text
result += "</text>"
return result
}
fileprivate func colorToSVG(_ color: Color) -> String {
if let c = SVGConstants.valueToColor(color.val) {
return "\(c)"
} else {
let r = color.r()
let g = color.g()
let b = color.b()
return "#\(String(format: "%02X%02X%02X", r, g, b))"
}
}
fileprivate func fillToSVG(_ fill: Fill?) -> String {
if let fillColor = fill as? Color {
var result = " fill=\"\(colorToSVG(fillColor))\""
if let opacity = alphaToSVGOpacity(fillColor.a()) {
result += " fill-opacity=\"\(opacity)\""
}
return result
}
return " fill=\"none\""
}
fileprivate func alphaToSVGOpacity(_ alpha: Int) -> String? {
if alpha == SVGDefaultOpacityValueAsAlpha {
return .none
}
return String(Double(alpha) / Double(SVGDefaultOpacityValueAsAlpha))
}
fileprivate func strokeToSVG(_ stroke: Stroke?) -> String {
var result = ""
if let strokeColor = stroke?.fill as? Color {
result += " stroke=\"\(colorToSVG(strokeColor))\""
if let opacity = alphaToSVGOpacity(strokeColor.a()) {
result += " stroke-opacity=\"\(opacity)\""
}
}
if let strokeWidth = stroke?.width {
result += " stroke-width=\"\(strokeWidth)\""
}
if let strokeCap = stroke?.cap {
if strokeCap != SVGConstants.defaultStrokeLineCap {
result += " stroke-linecap=\"\(strokeCap)\""
}
}
if let strokeJoin = stroke?.join {
if strokeJoin != SVGConstants.defaultStrokeLineJoin {
result += " stroke-linejoin=\"\(strokeJoin)\""
}
}
if let strokeDashes = stroke?.dashes, !strokeDashes.isEmpty {
let dashes = strokeDashes.map { String($0) }.joined(separator: ",")
result += " stroke-dasharray=\"\(dashes)\""
}
if let strokeOffset = stroke?.offset {
if strokeOffset != 0 {
result += " stroke-dashoffset=\"\(strokeOffset)\""
}
}
return result
}
fileprivate func isSignificantMatrixTransform(_ t: Transform) -> Bool {
for k in [t.m11, t.m12, t.m21, t.m22, t.dx, t.dy] {
if abs(k) > SVGEpsilon {
return true
}
}
return false
}
fileprivate func transformToSVG(_ place: Transform) -> String {
if [place.m11, place.m12, place.m21, place.m22] == [1.0, 0.0, 0.0, 1.0] {
if [place.dx, place.dy] == [0.0, 0.0] {
return ""
}
return " transform=\"translate(\(place.dx.serialize()),\(place.dy.serialize()))\" "
}
let matrixArgs = [place.m11, place.m12, place.m21, place.m22, place.dx, place.dy].map { $0.serialize() }.joined(separator: ",")
return " transform=\"matrix(\(matrixArgs))\" "
}
fileprivate func locusToSVG(_ locus: Locus) -> String {
switch locus {
case let arc as Arc:
return arcToSVG(arc)
case let polygon as Polygon:
return polygonToSVG(polygon)
case let polyline as Polyline:
return polylineToSVG(polyline)
case let path as Path:
return pathToSVG(path)
case let line as Line:
return lineToSVG(line)
case let ellipse as Ellipse:
return ellipseToSVG(ellipse)
case let circle as Circle:
return circleToSVG(circle)
case let roundRect as RoundRect:
return roundRectToSVG(roundRect)
case let rect as Rect:
return rectToSVG(rect)
case let transformedLocus as TransformedLocus:
return locusToSVG(transformedLocus.locus) + transformToSVG(transformedLocus.transform)
default:
return "\(SVGUndefinedTag) locus:\(locus)"
}
}
fileprivate var defs: String = ""
fileprivate func getDefs() -> String {
if defs.isEmpty {
return ""
}
return "<defs>" + defs + "</defs>"
}
fileprivate var clipPathCount: Int = 0
fileprivate func addClipToDefs(_ clip: Locus) {
clipPathCount += 1
defs += "<\(SVGClipPathName) id=\"\(SVGClipPathName)\(clipPathCount)\">" + locusToSVG(clip) + SVGGenericCloseTag + "</\(SVGClipPathName)>"
}
fileprivate var maskCount: Int = 0
fileprivate func addMaskToDefs(_ mask: Node) {
maskCount += 1
defs += "<\(SVGMaskName) id=\"\(SVGMaskName)\(maskCount)\">" + serialize(node: mask) + SVGGenericCloseTag + "</\(SVGMaskName)>"
}
fileprivate func idToSVG(_ tag: [String]) -> String {
guard !tag.isEmpty, let id = tag.first else {
return ""
}
return " id=\"\(id)\""
}
fileprivate func clipToSVG(_ clipLocus: Locus?) -> String {
guard let clip = clipLocus else {
return ""
}
addClipToDefs(clip)
return " clip-path=\"url(#\(SVGClipPathName)\(clipPathCount))\" "
}
fileprivate func maskToSVG(_ mask: Node?) -> String {
guard let mask = mask else {
return ""
}
addMaskToDefs(mask)
return " mask=\"url(#\(SVGMaskName)\(maskCount))\" "
}
fileprivate func macawShapeToSvgShape (macawShape: Shape) -> String {
let locus = macawShape.formVar.value
var result = locusToSVG(locus)
result += idToSVG(macawShape.tag)
result += clipToSVG(macawShape.clip)
result += maskToSVG(macawShape.mask)
result += fillToSVG(macawShape.fillVar.value)
result += strokeToSVG(macawShape.strokeVar.value)
result += transformToSVG(macawShape.place)
result += SVGGenericCloseTag
return result
}
fileprivate func serialize(node: Node) -> String {
if let shape = node as? Shape {
return macawShapeToSvgShape(macawShape: shape)
}
if let group = node as? Group {
var result = SVGGroupOpenTag
result += idToSVG(group.tag)
result += clipToSVG(group.clip)
result += transformToSVG(group.place)
result += SVGGenericEndTag
for child in group.contentsVar.value {
result += serialize(node: child)
}
result += SVGGroupCloseTag
return result
}
if let image = node as? Image {
return imageToSVG(image)
}
if let text = node as? Text {
return textToSVG(text)
}
return "SVGUndefinedTag \(node)"
}
fileprivate func serializeRootNode(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: " ")
let body = serialize(node: node)
result += getDefs() + body
result += SVGFooter
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).serializeRootNode(node: node)
}
}
extension Double {
func serialize() -> String {
let formatter = NumberFormatter()
formatter.minimumIntegerDigits = 1
formatter.maximumFractionDigits = 15
return abs(self.remainder(dividingBy: 1)) > 0.00001 ? formatter.string(from: NSNumber(value: self))! : String(Int(self.rounded()))
}
}