Skip to content

Commit

Permalink
Fix #123: Parse SVG filters to Macaw effects
Browse files Browse the repository at this point in the history
  • Loading branch information
f3dm76 committed May 16, 2018
1 parent e120be6 commit 3d668df
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 170 deletions.
210 changes: 108 additions & 102 deletions Macaw.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Source/model/draw/AlphaEffect.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
open class AlphaEffect: Effect {
}
16 changes: 0 additions & 16 deletions Source/model/draw/DropShadow.swift

This file was deleted.

5 changes: 3 additions & 2 deletions Source/model/draw/Effect.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Foundation

open class Effect {
open let input: Effect?

public init() {
public init(input: Effect?) {
self.input = input
}

}
5 changes: 2 additions & 3 deletions Source/model/draw/GaussianBlur.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import Foundation
open class GaussianBlur: Effect {

open let radius: Double
open let input: Effect?

public init(radius: Double = 0, input: Effect? = nil) {
public init(radius: Double = 0, input: Effect?) {
self.radius = radius
self.input = input
super.init(input: input)
}
}
11 changes: 11 additions & 0 deletions Source/model/draw/OffsetEffect.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
open class OffsetEffect: Effect {

open let dx: Double
open let dy: Double

public init(dx: Double = 0, dy: Double = 0, input: Effect?) {
self.dx = dx
self.dy = dy
super.init(input: input)
}
}
98 changes: 90 additions & 8 deletions Source/render/ShapeRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,104 @@ class ShapeRenderer: NodeRenderer {
observe(shape.strokeVar)
}

fileprivate func drawShape(in context: CGContext, opacity: Double) {
guard let shape = shape else { return }
setGeometry(shape.form, ctx: context)
var fillRule = FillRule.nonzero
if let path = shape.form as? Path {
fillRule = path.fillRule
}
drawPath(shape.fill, stroke: shape.stroke, ctx: context, opacity: opacity, fillRule: fillRule)
}

override func doRender(_ force: Bool, opacity: Double) {
guard let shape = shape else {
guard let shape = shape, let context = ctx.cgContext else { return }
if shape.fill == nil && shape.stroke == nil { return }

// no effects, just draw as usual
guard let effect = shape.effect else {
drawShape(in: context, opacity: opacity)
return
}

if shape.fill != nil || shape.stroke != nil {
setGeometry(shape.form, ctx: ctx.cgContext!)

var fillRule = FillRule.nonzero
if let path = shape.form as? Path {
fillRule = path.fillRule
var effects = [Effect]()
var next: Effect? = effect
while next != nil {
effects.append(next!)
next = next?.input
}

let offset = effects.filter { $0 is OffsetEffect }.first
let otherEffects = effects.filter { !($0 is OffsetEffect) }
if let offset = offset as? OffsetEffect {
let move = Transform(m11: 1, m12: 0, m21: 0, m22: 1, dx: offset.dx, dy: offset.dy)
context.concatenate(move.toCG())

if otherEffects.count == 0 {
// draw offset shape
drawShape(in: context, opacity: opacity)
} else {
// apply other effects to offset shape
applyEffects(otherEffects, opacity: opacity)
}
drawPath(shape.fill, stroke: shape.stroke, ctx: ctx.cgContext!, opacity: opacity, fillRule: fillRule)

// move back and draw the shape itself
context.concatenate(move.invert()!.toCG())
drawShape(in: context, opacity: opacity)
} else {
// draw the shape
drawShape(in: context, opacity: opacity)

// apply other effects to shape
applyEffects(otherEffects, opacity: opacity)
}
}

fileprivate func applyEffects(_ effects: [Effect], opacity: Double) {
guard let shape = shape, let context = ctx.cgContext else { return }
for effect in effects {
if let blur = effect as? GaussianBlur {
let shadowInset = min(blur.radius * 6 + 1, 150)
guard let shapeImage = saveToImage(shape: shape, shadowInset: shadowInset, opacity: opacity)?.cgImage else { return }

guard let filteredImage = applyBlur(shapeImage, blur: blur) else { return }

guard let bounds = shape.bounds() else { return }
context.draw(filteredImage, in: CGRect(x: bounds.x - shadowInset / 2, y: bounds.y - shadowInset / 2, width: bounds.w + shadowInset, height: bounds.h + shadowInset))
}
}
}

fileprivate func applyBlur(_ image: CGImage, blur: GaussianBlur) -> CGImage? {
let image = CIImage(cgImage: image)
guard let filter = CIFilter(name: "CIGaussianBlur") else { return .none }
filter.setDefaults()
filter.setValue(Int(blur.radius), forKey: kCIInputRadiusKey)
filter.setValue(image, forKey: kCIInputImageKey)

let context = CIContext(options: nil)
let imageRef = context.createCGImage(filter.outputImage!, from: image.extent)
return imageRef
}

fileprivate func saveToImage(shape: Shape, shadowInset: Double, opacity: Double) -> MImage? {
guard let size = shape.bounds() else { return .none }
MGraphicsBeginImageContextWithOptions(CGSize(width: size.w + shadowInset, height: size.h + shadowInset), false, 1)

guard let tempContext = MGraphicsGetCurrentContext() else { return .none }

if (shape.fill != nil || shape.stroke != nil) {
// flip y-axis and leave space for the blur
tempContext.translateBy(x: CGFloat(shadowInset / 2 - size.x), y: CGFloat(size.h + shadowInset / 2 + size.y))
tempContext.scaleBy(x: 1, y: -1)
drawShape(in: tempContext, opacity: opacity)
}

let img = MGraphicsGetImageFromCurrentImageContext()
MGraphicsEndImageContext()
return img
}

override func doFindNodeAt(location: CGPoint, ctx: CGContext) -> Node? {
guard let shape = shape else {
return .none
Expand Down
130 changes: 91 additions & 39 deletions Source/svg/SVGParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ open class SVGParser {
fileprivate var defFills = [String: Fill]()
fileprivate var defMasks = [String: Shape]()
fileprivate var defClip = [String: Locus]()
fileprivate var defEffects = [String: Effect]()

fileprivate enum PathCommandType {
case moveTo
Expand Down Expand Up @@ -171,8 +172,6 @@ open class SVGParser {
if let id = element.allAttributes["id"]?.text, let clip = parseClip(node) {
self.defClip[id] = clip
}
case "linearGradient", "radialGradient":
parseDefinition(node)
case "style", "defs":
// do nothing - it was parsed on first iteration
return .none
Expand Down Expand Up @@ -211,40 +210,49 @@ open class SVGParser {
}

private func parseDefinition(_ child: XMLIndexer) {
guard let id = child.element?.allAttributes["id"]?.text else {
guard let id = child.element?.allAttributes["id"]?.text, let element = child.element else {
return
}
if let fill = parseFill(child) {

if element.name == "fill", let fill = parseFill(child) {
defFills[id] = fill
}

else if let _ = parseNode(child) {
// TODO we don't really need to parse node
defNodes[id] = child
}

else if let mask = parseMask(child) {
} else if element.name == "mask", let mask = parseMask(child) {
defMasks[id] = mask
}

else if let clip = parseClip(child) {
} else if element.name == "filter", let effect = parseEffect(child) {
defEffects[id] = effect
} else if element.name == "clip", let clip = parseClip(child) {
defClip[id] = clip
} else if let _ = parseNode(child) {
// TODO we don't really need to parse node
defNodes[id] = child
}
}


fileprivate func parseElement(_ node: XMLIndexer, groupStyle: [String: String] = [:]) -> Node? {
guard let element = node.element else { return .none }

let styleAttributes = getStyleAttributes(groupStyle, element: element)
if styleAttributes["display"] == "none" {
let nodeStyle = getStyleAttributes(groupStyle, element: element)
if nodeStyle["display"] == "none" {
return .none
}
if styleAttributes["visibility"] == "hidden" {
if nodeStyle["visibility"] == "hidden" {
return .none
}

guard let parsedNode = parseElementInternal(node, groupStyle: nodeStyle) else { return .none }

if let filterString = element.allAttributes["filter"]?.text ?? nodeStyle["filter"], let filterId = parseIdFromUrl(filterString), let effect = defEffects[filterId] {
parsedNode.effect = effect
}

return parsedNode
}

fileprivate func parseElementInternal(_ node: XMLIndexer, groupStyle: [String: String] = [:]) -> Node? {
guard let element = node.element else { return .none }
let id = node.element?.allAttributes["id"]?.text

let styleAttributes = groupStyle
let position = getPosition(element)
switch element.name {
case "path":
Expand Down Expand Up @@ -285,6 +293,14 @@ open class SVGParser {
stroke: getStroke(styleAttributes, groupStyle: styleAttributes), opacity: getOpacity(styleAttributes), fontName: getFontName(styleAttributes), fontSize: getFontSize(styleAttributes), fontWeight: getFontWeight(styleAttributes), pos: position)
case "use":
return parseUse(node, groupStyle: styleAttributes, place: position)
case "linearGradient", "radialGradient", "fill":
if let fill = parseFill(node), let id = id {
defFills[id] = fill
}
case "filter":
if let effect = parseEffect(node), let id = id {
defEffects[id] = effect
}
case "mask":
break
default:
Expand Down Expand Up @@ -558,13 +574,8 @@ open class SVGParser {
if fillColor.hasPrefix("rgb") {
let color = parseRGBNotation(colorString: fillColor)
return hasFillOpacity ? color.with(a: opacity) : color
} else if fillColor.hasPrefix("url") {
let index = fillColor.index(fillColor.startIndex, offsetBy: 4)
let id = String(fillColor.suffix(from: index))
.replacingOccurrences(of: "(", with: "")
.replacingOccurrences(of: ")", with: "")
.replacingOccurrences(of: "#", with: "")
return defFills[id]
} else if let colorId = parseIdFromUrl(fillColor) {
return defFills[colorId]
} else {
return createColor(fillColor.replacingOccurrences(of: " ", with: ""), opacity: opacity)
}
Expand All @@ -591,13 +602,8 @@ open class SVGParser {
fill = color.with(a: opacity)
} else if strokeColor.hasPrefix("rgb") {
fill = parseRGBNotation(colorString: strokeColor)
} else if strokeColor.hasPrefix("url") {
let index = strokeColor.index(strokeColor.startIndex, offsetBy: 4)
let id = String(strokeColor.suffix(from: index))
.replacingOccurrences(of: "(", with: "")
.replacingOccurrences(of: ")", with: "")
.replacingOccurrences(of: "#", with: "")
fill = defFills[id]
} else if let colorId = parseIdFromUrl(strokeColor) {
fill = defFills[colorId]
} else {
fill = createColor(strokeColor.replacingOccurrences(of: " ", with: ""), opacity: opacity)
}
Expand Down Expand Up @@ -1000,6 +1006,42 @@ open class SVGParser {
return path
}

fileprivate func parseEffect(_ filterNode: XMLIndexer) -> Effect? {
let defaultSource = "SourceGraphic"
var effects = [String: Effect]()
for child in filterNode.children.reversed() {
guard let element = child.element else { continue }

let filterIn = element.allAttributes["in"]?.text ?? defaultSource
let filterOut = element.allAttributes["result"]?.text ?? ""
let currentEffect = effects[filterOut]
effects.removeValue(forKey: filterOut)

switch element.name {
case "feOffset":
if let dx = getDoubleValue(element, attribute: "dx"), let dy = getDoubleValue(element, attribute: "dy") {
effects[filterIn] = OffsetEffect(dx: dx, dy: dy, input: currentEffect)
}
case "feGaussianBlur":
if let radius = getDoubleValue(element, attribute: "stdDeviation") {
effects[filterIn] = GaussianBlur(radius: radius, input: currentEffect)
}
case "feBlend":
if let filterIn2 = element.allAttributes["in2"]?.text {
if filterIn2 == defaultSource {
effects[filterIn] = nil
} else if filterIn == defaultSource {
effects[filterIn2] = nil
}
}
default:
print("SVG parsing error. Filter \(element.name) not supported")
continue
}
}
return effects.first?.value
}

fileprivate func parseMask(_ mask: XMLIndexer) -> Shape? {
guard let element = mask.element else {
return .none
Expand Down Expand Up @@ -1195,6 +1237,13 @@ open class SVGParser {
return .none
}

fileprivate func parseIdFromUrl(_ urlString: String) -> String? {
if urlString.hasPrefix("url") {
return urlString.substringWithOffset(fromStart: 5, fromEnd: 1)
}
return .none
}

fileprivate func getDoubleValue(_ element: SWXMLHash.XMLElement, attribute: String) -> Double? {
guard let attributeValue = element.allAttributes[attribute]?.text else {
return .none
Expand Down Expand Up @@ -1307,12 +1356,7 @@ open class SVGParser {
}

fileprivate func getClipPath(_ attributes: [String: String]) -> Locus? {
if let clipPath = attributes["clip-path"] {
let index = clipPath.index(clipPath.startIndex, offsetBy: 4)
let id = String(clipPath.suffix(from: index))
.replacingOccurrences(of: "(", with: "")
.replacingOccurrences(of: ")", with: "")
.replacingOccurrences(of: "#", with: "")
if let clipPath = attributes["clip-path"], let id = parseIdFromUrl(clipPath) {
if let locus = defClip[id] {
return locus
}
Expand Down Expand Up @@ -1555,3 +1599,11 @@ private class PathDataReader {
}

}

fileprivate extension String {
func substringWithOffset(fromStart: Int, fromEnd: Int) -> String {
let start = index(startIndex, offsetBy: fromStart)
let end = index(endIndex, offsetBy: -fromEnd)
return String(self[start..<end])
}
}

0 comments on commit 3d668df

Please sign in to comment.