1
1
mirror of https://github.com/exyte/Macaw.git synced 2024-09-11 05:05:23 +03:00

Add patterns

Update Pattern model

Fix

Cherry pick

Last cherry pick

Fix

Merge fixes
This commit is contained in:
Alisa Mylnikova 2018-06-08 16:16:38 +07:00
parent a3a9ff5c5d
commit 0d2bd69d75
9 changed files with 407 additions and 30 deletions

View File

@ -464,6 +464,10 @@
5B7E79CF20CBE69700C50BCF /* masking-path-02-b-manual.svg in Resources */ = {isa = PBXBuildFile; fileRef = 5B7E79CD20CBE69700C50BCF /* masking-path-02-b-manual.svg */; };
5B7E79DE20D2781A00C50BCF /* masking-intro-01-f-manual.reference in Resources */ = {isa = PBXBuildFile; fileRef = 5B7E79DC20D2781A00C50BCF /* masking-intro-01-f-manual.reference */; };
5B7E79DF20D2781A00C50BCF /* masking-intro-01-f-manual.svg in Resources */ = {isa = PBXBuildFile; fileRef = 5B7E79DD20D2781A00C50BCF /* masking-intro-01-f-manual.svg */; };
5B7E79C020CA7E9300C50BCF /* Pattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B7E79BF20CA7E9300C50BCF /* Pattern.swift */; };
5B7E79C120CA7E9300C50BCF /* Pattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B7E79BF20CA7E9300C50BCF /* Pattern.swift */; };
5B7E79C420CA7F1B00C50BCF /* pservers-grad-03-b-manual.svg in Resources */ = {isa = PBXBuildFile; fileRef = 5B7E79C220CA7F1A00C50BCF /* pservers-grad-03-b-manual.svg */; };
5B7E79C520CA7F1B00C50BCF /* pservers-grad-03-b-manual.reference in Resources */ = {isa = PBXBuildFile; fileRef = 5B7E79C320CA7F1B00C50BCF /* pservers-grad-03-b-manual.reference */; };
5BAE201F208E1211006BF277 /* SVGCanvas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BAE201E208E1211006BF277 /* SVGCanvas.swift */; };
5BAE2038208E163D006BF277 /* polyline.reference in Resources */ = {isa = PBXBuildFile; fileRef = 5BAE2022208E1637006BF277 /* polyline.reference */; };
5BAE2039208E163D006BF277 /* polygon.reference in Resources */ = {isa = PBXBuildFile; fileRef = 5BAE2023208E1637006BF277 /* polygon.reference */; };
@ -878,6 +882,9 @@
5B7E79CD20CBE69700C50BCF /* masking-path-02-b-manual.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "masking-path-02-b-manual.svg"; sourceTree = "<group>"; };
5B7E79DC20D2781A00C50BCF /* masking-intro-01-f-manual.reference */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "masking-intro-01-f-manual.reference"; sourceTree = "<group>"; };
5B7E79DD20D2781A00C50BCF /* masking-intro-01-f-manual.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "masking-intro-01-f-manual.svg"; sourceTree = "<group>"; };
5B7E79BF20CA7E9300C50BCF /* Pattern.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pattern.swift; sourceTree = "<group>"; };
5B7E79C220CA7F1A00C50BCF /* pservers-grad-03-b-manual.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "pservers-grad-03-b-manual.svg"; sourceTree = "<group>"; };
5B7E79C320CA7F1B00C50BCF /* pservers-grad-03-b-manual.reference */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "pservers-grad-03-b-manual.reference"; sourceTree = "<group>"; };
5BAE201E208E1211006BF277 /* SVGCanvas.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGCanvas.swift; sourceTree = "<group>"; };
5BAE2022208E1637006BF277 /* polyline.reference */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = polyline.reference; sourceTree = "<group>"; };
5BAE2023208E1637006BF277 /* polygon.reference */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = polygon.reference; sourceTree = "<group>"; };
@ -1193,6 +1200,7 @@
5B6E191A20AC58F800454E7E /* LineCap.swift */,
5B6E191720AC58F800454E7E /* LineJoin.swift */,
5B6E191020AC58F700454E7E /* OffsetEffect.swift */,
5B7E79BF20CA7E9300C50BCF /* Pattern.swift */,
5B6E191220AC58F700454E7E /* RadialGradient.swift */,
5B6E191920AC58F800454E7E /* Stop.swift */,
5B6E191E20AC58F900454E7E /* Stroke.swift */,
@ -1472,6 +1480,8 @@
5B37139220BE95D6004BB6EE /* pservers-grad-01-b-manual.svg */,
5B37139320BE95D6004BB6EE /* pservers-grad-02-b-manual.reference */,
5B37139420BE95D6004BB6EE /* pservers-grad-02-b-manual.svg */,
5B7E79C320CA7F1B00C50BCF /* pservers-grad-03-b-manual.reference */,
5B7E79C220CA7F1A00C50BCF /* pservers-grad-03-b-manual.svg */,
5B37139120BE95D6004BB6EE /* pservers-grad-07-b-manual.reference */,
5B37139520BE95D7004BB6EE /* pservers-grad-07-b-manual.svg */,
5B1AE1F020B6A669007EECCB /* pservers-grad-stops-01-f-manual.reference */,
@ -1858,6 +1868,7 @@
5B1AE29A20B6A669007EECCB /* struct-frag-02-t-manual.svg in Resources */,
5B1AE2A620B6A669007EECCB /* text-fonts-01-t-manual.svg in Resources */,
5B1AE28520B6A669007EECCB /* paths-data-19-f-manual.reference in Resources */,
5B7E79C420CA7F1B00C50BCF /* pservers-grad-03-b-manual.svg in Resources */,
5B1AE27F20B6A669007EECCB /* painting-stroke-07-t-manual.reference in Resources */,
5B1AE23320B6A669007EECCB /* paths-data-06-t-manual.reference in Resources */,
5B1AE2C820B6A669007EECCB /* paths-data-19-f-manual.svg in Resources */,
@ -1934,6 +1945,7 @@
5B1AE26720B6A669007EECCB /* coords-coord-02-t-manual.reference in Resources */,
5B1AE29220B6A669007EECCB /* coords-transformattr-03-f-manual.reference in Resources */,
5B1AE25A20B6A669007EECCB /* painting-stroke-06-t-manual.svg in Resources */,
5B7E79C520CA7F1B00C50BCF /* pservers-grad-03-b-manual.reference in Resources */,
5B1AE24320B6A669007EECCB /* painting-stroke-07-t-manual.svg in Resources */,
5B1AE27B20B6A669007EECCB /* painting-fill-02-t-manual.svg in Resources */,
5B37139A20BE95D7004BB6EE /* pservers-grad-02-b-manual.svg in Resources */,
@ -2053,6 +2065,7 @@
57614B2E1F83D15600875933 /* CombineAnimation.swift in Sources */,
57614B2F1F83D15600875933 /* TransformHashable.swift in Sources */,
57614B301F83D15600875933 /* MoveTo.swift in Sources */,
5B7E79C120CA7E9300C50BCF /* Pattern.swift in Sources */,
5B6E193A20AC58F900454E7E /* Drawable.swift in Sources */,
57614B311F83D15600875933 /* NodeRenderer.swift in Sources */,
57614B331F83D15600875933 /* Animation.swift in Sources */,
@ -2191,6 +2204,7 @@
57E5E16C1E3B393900D1CB28 /* CombineAnimation.swift in Sources */,
57E5E1661E3B393900D1CB28 /* TransformHashable.swift in Sources */,
57E5E1921E3B393900D1CB28 /* MoveTo.swift in Sources */,
5B7E79C020CA7E9300C50BCF /* Pattern.swift in Sources */,
57E5E1A51E3B393900D1CB28 /* NodeRenderer.swift in Sources */,
5B6E193920AC58F900454E7E /* Drawable.swift in Sources */,
57E5E1541E3B393900D1CB28 /* Animation.swift in Sources */,

View File

@ -173,7 +173,11 @@ class MacawSVGTests: XCTestCase {
let nodeContent = String(data: getJSONData(node: node), encoding: String.Encoding.utf8)
XCTAssertEqual(nodeContent, referenceContent)
if nodeContent != referenceContent {
let referencePath = writeToFile(string: referenceContent, fileName: referenceFile + "_reference.txt")
let _ = writeToFile(string: nodeContent!, fileName: referenceFile + "_incorrect.txt")
XCTFail("Not equal, see both files in \(String(describing: referencePath!.deletingLastPathComponent().path))")
}
} else {
XCTFail("No file \(referenceFile)")
}
@ -196,8 +200,9 @@ class MacawSVGTests: XCTestCase {
do {
let bundle = Bundle(for: type(of: TestUtils()))
let node = try SVGParser.parse(resource: testResourcePath, fromBundle: bundle)
let path = testResourcePath + ".reference"
try getJSONData(node: node).write(to: URL(fileURLWithPath: path))
let fileName = testResourcePath + ".reference"
let jsonData = getJSONData(node: node)
print("New reference file in \(String(writeToFile(data: jsonData, fileName: fileName)!.path))")
} catch {
XCTFail(error.localizedDescription)
}
@ -220,6 +225,34 @@ class MacawSVGTests: XCTestCase {
}
}
func writeToFile(string: String, fileName: String) -> URL? {
guard let directory = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) as NSURL else {
return .none
}
do {
let path = directory.appendingPathComponent("\(fileName)")!
try string.write(to: path, atomically: true, encoding: String.Encoding.utf8)
return path
} catch {
print(error.localizedDescription)
return .none
}
}
func writeToFile(data: Data, fileName: String) -> URL? {
guard let directory = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) as NSURL else {
return .none
}
do {
let path = directory.appendingPathComponent("\(fileName)")!
try data.write(to: URL(fileURLWithPath: fileName))
return path
} catch {
print(error.localizedDescription)
return .none
}
}
func testTextAlign01() {
validateJSON("text-align-01-b-manual")
}
@ -583,7 +616,7 @@ class MacawSVGTests: XCTestCase {
func testShapesGrammar01() {
validateJSON("shapes-grammar-01-f-manual")
}
func testMaskingPath02() {
validateJSON("masking-path-02-b-manual")
}
@ -591,4 +624,8 @@ class MacawSVGTests: XCTestCase {
func testMaskingIntro01() {
validateJSON("masking-intro-01-f-manual")
}
func testPserversGrad03() {
validateJSON("pservers-grad-03-b-manual")
}
}

View File

@ -15,7 +15,7 @@ The rest 306 tests can be split into following categories:
* 7.8% images (24) [wpt issue](https://github.com/web-platform-tests/wpt/issues/11178)
* 2.6% markers (8) [#392](https://github.com/exyte/Macaw/issues/392)
* 19.9% text (61) [#391](https://github.com/exyte/Macaw/issues/391)
* 24.9% blocked by issues (76)
* 25.2% blocked by issues (77)
Status of each test:
@ -186,7 +186,7 @@ Status of each test:
|[paths-data-20-f-manual](w3cSVGTests/paths-data-20-f-manual.svg) | [#395](https://github.com/exyte/Macaw/issues/395) |
|[pservers-grad-01-b-manual](w3cSVGTests/pservers-grad-01-b-manual.svg) | ✅ |
|[pservers-grad-02-b-manual](w3cSVGTests/pservers-grad-02-b-manual.svg) | ✅ |
|[pservers-grad-03-b-manual](w3cSVGTests/pservers-grad-03-b-manual.svg) | [#396](https://github.com/exyte/Macaw/issues/396) |
|[pservers-grad-03-b-manual](w3cSVGTests/pservers-grad-03-b-manual.svg) | |
|[pservers-grad-04-b-manual](w3cSVGTests/pservers-grad-04-b-manual.svg) | [#396](https://github.com/exyte/Macaw/issues/396) |
|[pservers-grad-05-b-manual](w3cSVGTests/pservers-grad-05-b-manual.svg) | [#396](https://github.com/exyte/Macaw/issues/396) |
|[pservers-grad-06-b-manual](w3cSVGTests/pservers-grad-06-b-manual.svg) | [#396](https://github.com/exyte/Macaw/issues/396) |

View File

@ -0,0 +1,121 @@
{
"contents" : [
{
"contents" : [
{
"form" : {
"h" : 80,
"type" : "Rect",
"w" : 440,
"x" : 20,
"y" : 20
},
"node" : "Shape"
},
{
"align" : "min",
"baseline" : "bottom",
"fill" : {
"type" : "Color",
"val" : 0
},
"font" : {
"name" : "SVGFreeSansASCII,sans-serif",
"size" : 30,
"weight" : "normal"
},
"node" : "Text",
"place" : "1, 0, 0, 1, 20, 130",
"text" : "Pattern fill."
},
{
"form" : {
"h" : 80,
"type" : "Rect",
"w" : 440,
"x" : 20,
"y" : 160
},
"node" : "Shape"
},
{
"align" : "min",
"baseline" : "bottom",
"fill" : {
"type" : "Color",
"val" : 0
},
"font" : {
"name" : "SVGFreeSansASCII,sans-serif",
"size" : 30,
"weight" : "normal"
},
"node" : "Text",
"place" : "1, 0, 0, 1, 20, 270",
"text" : "Referencing pattern fill below."
}
],
"node" : "Group"
},
{
"contents" : [
{
"align" : "min",
"baseline" : "bottom",
"fill" : {
"type" : "Color",
"val" : 0
},
"font" : {
"name" : "SVGFreeSansASCII,sans-serif",
"size" : 32,
"weight" : "normal"
},
"node" : "Text",
"place" : "1, 0, 0, 1, 10, 340",
"text" : "$Revision: 1.8 $"
}
],
"node" : "Group"
},
{
"form" : {
"h" : 358,
"type" : "Rect",
"w" : 478,
"x" : 1,
"y" : 1
},
"node" : "Shape",
"stroke" : {
"cap" : "butt",
"dashes" : [
],
"fill" : {
"type" : "Color",
"val" : 0
},
"join" : "miter",
"width" : 1
}
}
],
"layout" : {
"scalingMode" : "meet",
"svgSize" : {
"height" : "100.0%",
"width" : "100.0%"
},
"viewBox" : {
"h" : 360,
"type" : "Rect",
"w" : 480,
"x" : 0,
"y" : 0
},
"xAligningMode" : "mid",
"yAligningMode" : "mid"
},
"node" : "Canvas"
}

View File

@ -0,0 +1,77 @@
<svg version="1.1" baseProfile="basic" id="svg-root"
width="100%" height="100%" viewBox="0 0 480 360"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!--======================================================================-->
<!--= SVG 1.1 2nd Edition Test Case =-->
<!--======================================================================-->
<!--= Copyright 2009 World Wide Web Consortium, (Massachusetts =-->
<!--= Institute of Technology, European Research Consortium for =-->
<!--= Informatics and Mathematics (ERCIM), Keio University). =-->
<!--= All Rights Reserved. =-->
<!--= See http://www.w3.org/Consortium/Legal/. =-->
<!--======================================================================-->
<d:SVGTestCase xmlns:d="http://www.w3.org/2000/02/svg/testsuite/description/"
template-version="1.4" reviewer="SVGWG" author="Haroon Sheikh" status="accepted"
version="$Revision: 1.8 $" testname="$RCSfile: pservers-grad-03-b.svg,v $">
<d:testDescription xmlns="http://www.w3.org/1999/xhtml" href="http://www.w3.org/TR/SVG11/pservers.html#Gradients">
<p>
Test that the viewer can handle the xlink:href attribute on
patterns.
</p>
<p>
There are two rectangles with a pattern fill made
up of 4 rectangles. The pattern definition of the lower one references the pattern definition
of the upper one, using the xlink:href attribute. Because
the particular way that the patterns and rectangles are
defined in this test case, the two fills will appear the
same - the rectangles are positioned on pattern-size
boundaries, so that the offsets into the pattern at the left
edges of the respective rectangles is identical.
</p>
</d:testDescription>
<d:operatorScript xmlns="http://www.w3.org/1999/xhtml">
<p>
Run the test. No interaction required.
</p>
</d:operatorScript>
<d:passCriteria xmlns="http://www.w3.org/1999/xhtml">
<p>The test passes if the rendering matches the reference image, except
for any differences in text due to CSS2 rules. Note that the top rectangle must
look identical to the bottom rectangle.</p>
</d:passCriteria>
</d:SVGTestCase>
<title id="test-title">$RCSfile: pservers-grad-03-b.svg,v $</title>
<defs>
<font-face font-family="SVGFreeSansASCII" unicode-range="U+0-7F">
<font-face-src>
<font-face-uri xlink:href="../resources/SVGFreeSans.svg#ascii"/>
</font-face-src>
</font-face>
</defs>
<g id="test-body-content" font-family="SVGFreeSansASCII,sans-serif" font-size="18">
<!-- ====================================================================== -->
<!-- Pattern filled rectangles using a xlink:href to another pattern fill -->
<!-- ====================================================================== -->
<pattern id="Pat3a" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
<rect x="0" y="0" width="10" height="10" fill="#93D"/>
<rect x="10" y="0" width="10" height="10" fill="green"/>
<rect x="0" y="10" width="10" height="10" fill="blue"/>
<rect x="10" y="10" width="10" height="10" fill="yellow"/>
</pattern>
<pattern id="Pat3b" xlink:href="#Pat3a" width="20" height="20"/>
<rect x="20" y="20" width="440" height="80" fill="url(#Pat3a)"/>
<text font-size="30" x="20" y="130">Pattern fill.</text>
<rect x="20" y="160" width="440" height="80" fill="url(#Pat3b)"/>
<text font-size="30" x="20" y="270">Referencing pattern fill below.</text>
</g>
<g font-family="SVGFreeSansASCII,sans-serif" font-size="32">
<text id="revision" x="10" y="340" stroke="none" fill="black">$Revision: 1.8 $</text>
</g>
<rect id="test-frame" x="1" y="1" width="478" height="358" fill="none" stroke="#000000"/>
<!-- comment out this watermark once the test is approved -->
<!--<g id="draft-watermark">
<rect x="1" y="1" width="478" height="20" fill="red" stroke="black" stroke-width="1"/>
<text font-family="SVGFreeSansASCII,sans-serif" font-weight="bold" font-size="20" x="240"
text-anchor="middle" y="18" stroke-width="0.5" stroke="black" fill="white">DRAFT</text>
</g>-->
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,12 @@
open class Pattern: Fill {
public let content: Node
public let bounds: Rect
public let userSpace: Bool
init(content: Node, bounds: Rect, userSpace: Bool = false) {
self.content = content
self.bounds = bounds
self.userSpace = userSpace
}
}

View File

@ -129,6 +129,8 @@ class ShapeRenderer: NodeRenderer {
ctx!.setFillColor(color.toCG())
} else if let gradient = fill as? Gradient {
drawGradient(gradient, ctx: ctx, opacity: opacity)
} else if let pattern = fill as? Pattern {
drawPattern(pattern, ctx: ctx, opacity: opacity)
} else {
print("Unsupported fill: \(fill)")
}
@ -169,6 +171,25 @@ class ShapeRenderer: NodeRenderer {
drawGradient(gradient, ctx: ctx, opacity: opacity)
}
fileprivate func drawPattern(_ pattern: Pattern, ctx: CGContext?, opacity: Double) {
guard let shape = shape else {
return
}
if !pattern.userSpace {
BoundsUtils.applyTransformToNodeInRespectiveCoords(respectiveNode: pattern.content, absoluteLocus: shape.form)
}
let renderer = RenderUtils.createNodeRenderer(pattern.content, view: view, animationCache: animationCache)
var patternBounds = pattern.bounds
if !pattern.userSpace {
let boundsTranform = BoundsUtils.transformForLocusInRespectiveCoords(respectiveLocus: pattern.bounds, absoluteLocus: shape.form)
patternBounds = pattern.bounds.applying(boundsTranform)
}
let tileImage = renderer.renderToImage(bounds: patternBounds, inset: 0)
ctx!.clip()
ctx?.draw(tileImage.cgImage!, in: patternBounds.toCG(), byTiling: true)
}
fileprivate func drawGradient(_ gradient: Gradient, ctx: CGContext?, opacity: Double) {
ctx!.saveGState()
var colors: [CGColor] = []

View File

@ -154,8 +154,8 @@ open class SVGParser {
}
if let id = element.allAttributes["id"]?.text {
switch element.name {
case "linearGradient", "radialGradient", "fill":
defFills[id] = parseFill(node)
case "linearGradient", "radialGradient", "pattern", "fill":
defFills[id] = try parseFill(node)
case "mask":
defMasks[id] = try parseMask(node)
case "filter":
@ -272,37 +272,38 @@ open class SVGParser {
let mask = try getMask(style, locus: path)
return Shape(form: path, fill: getFillColor(style, groupStyle: style), stroke: getStroke(style, groupStyle: style), place: position, opacity: getOpacity(style), clip: getClipPath(style, locus: path), mask: mask, tag: getTag(element))
return Shape(form: path, fill: getFillColor(style, groupStyle: style, locus: path), stroke: getStroke(style, groupStyle: style), place: position, opacity: getOpacity(style), clip: getClipPath(style, locus: path), mask: mask, tag: getTag(element))
}
case "line":
if let line = parseLine(node) {
let mask = try getMask(style, locus: line)
return Shape(form: line, fill: getFillColor(style, groupStyle: style), stroke: getStroke(style, groupStyle: style), place: position, opacity: getOpacity(style), clip: getClipPath(style, locus: line), mask: mask, tag: getTag(element))
return Shape(form: line, fill: getFillColor(style, groupStyle: style, locus: line), stroke: getStroke(style, groupStyle: style), place: position, opacity: getOpacity(style), clip: getClipPath(style, locus: line), mask: mask, tag: getTag(element))
}
case "rect":
if let rect = parseRect(node) {
let mask = try getMask(style, locus: rect)
return Shape(form: rect, fill: getFillColor(style, groupStyle: style), stroke: getStroke(style, groupStyle: style), place: position, opacity: getOpacity(style), clip: getClipPath(style, locus: rect), mask: mask, tag: getTag(element))
return Shape(form: rect, fill: getFillColor(style, groupStyle: style, locus: rect), stroke: getStroke(style, groupStyle: style), place: position, opacity: getOpacity(style), clip: getClipPath(style, locus: rect), mask: mask, tag: getTag(element))
}
case "circle":
if let circle = parseCircle(node) {
let mask = try getMask(style, locus: circle)
return Shape(form: circle, fill: getFillColor(style, groupStyle: style), stroke: getStroke(style, groupStyle: style), place: position, opacity: getOpacity(style), clip: getClipPath(style, locus: circle), mask: mask, tag: getTag(element))
return Shape(form: circle, fill: getFillColor(style, groupStyle: style, locus: circle), stroke: getStroke(style, groupStyle: style), place: position, opacity: getOpacity(style), clip: getClipPath(style, locus: circle), mask: mask, tag: getTag(element))
}
case "ellipse":
if let ellipse = parseEllipse(node) {
let mask = try getMask(style, locus: ellipse)
return Shape(form: ellipse, fill: getFillColor(style, groupStyle: style), stroke: getStroke(style, groupStyle: style), place: position, opacity: getOpacity(style), clip: getClipPath(style, locus: ellipse), mask: mask, tag: getTag(element))
return Shape(form: ellipse, fill: getFillColor(style, groupStyle: style, locus: ellipse), stroke: getStroke(style, groupStyle: style), place: position, opacity: getOpacity(style), clip: getClipPath(style, locus: ellipse), mask: mask, tag: getTag(element))
}
case "polygon":
if let polygon = parsePolygon(node) {
let mask = try getMask(style, locus: polygon)
return Shape(form: polygon, fill: getFillColor(style, groupStyle: style), stroke: getStroke(style, groupStyle: style), place: position, opacity: getOpacity(style), clip: getClipPath(style, locus: polygon), mask: mask, tag: getTag(element))
return Shape(form: polygon, fill: getFillColor(style, groupStyle: style, locus: polygon), stroke: getStroke(style, groupStyle: style), place: position, opacity: getOpacity(style), clip: getClipPath(style, locus: polygon), mask: mask, tag: getTag(element))
}
case "polyline":
if let polyline = parsePolyline(node) {
let mask = try getMask(style, locus: polyline)
return Shape(form: polyline, fill: getFillColor(style, groupStyle: style), stroke: getStroke(style, groupStyle: style), place: position, opacity: getOpacity(style), clip: getClipPath(style, locus: polyline), mask: mask, tag: getTag(element))
return Shape(form: polyline, fill: getFillColor(style, groupStyle: style, locus: polyline), stroke: getStroke(style, groupStyle: style), place: position, opacity: getOpacity(style), clip: getClipPath(style, locus: polyline), mask: mask, tag: getTag(element))
}
case "image":
return parseImage(node, opacity: getOpacity(style), pos: position, clip: getClipPath(style, locus: nil))
@ -322,7 +323,7 @@ open class SVGParser {
return .none
}
fileprivate func parseFill(_ fill: XMLIndexer) -> Fill? {
fileprivate func parseFill(_ fill: XMLIndexer) throws -> Fill? {
guard let element = fill.element else {
return .none
}
@ -332,11 +333,62 @@ open class SVGParser {
return parseLinearGradient(fill, groupStyle: style)
case "radialGradient":
return parseRadialGradient(fill, groupStyle: style)
case "pattern":
return try parsePattern(fill, groupStyle: style)
default:
return .none
}
}
fileprivate func parsePattern(_ pattern: XMLIndexer, groupStyle: [String: String] = [:]) throws -> Fill? {
guard let element = pattern.element else {
return .none
}
var parentPattern: UserSpacePattern?
if let link = element.allAttributes["xlink:href"]?.text.replacingOccurrences(of: " ", with: ""), link.hasPrefix("#") {
let id = link.replacingOccurrences(of: "#", with: "")
parentPattern = defFills[id] as? UserSpacePattern
}
let x = getDoubleValue(element, attribute: "x") ?? parentPattern?.bounds.x ?? 0
let y = getDoubleValue(element, attribute: "y") ?? parentPattern?.bounds.y ?? 0
let w = getDoubleValue(element, attribute: "width") ?? parentPattern?.bounds.w ?? 0
let h = getDoubleValue(element, attribute: "height") ?? parentPattern?.bounds.h ?? 0
let bounds = Rect(x: x, y: y, w: w, h: h)
var userSpace = parentPattern?.userSpace ?? false
if let units = element.allAttributes["patternUnits"]?.text, units == "userSpaceOnUse" {
userSpace = true
}
var contentUserSpace = parentPattern?.contentUserSpace ?? true
if let units = element.allAttributes["patternContentUnits"]?.text, units == "objectBoundingBox" {
contentUserSpace = false
}
var contentNode: Node?
if pattern.children.isEmpty {
if let parentPattern = parentPattern {
contentNode = parentPattern.content
}
} else if pattern.children.count == 1 {
if let shape = try parseNode(pattern.children.first!) as? Shape {
contentNode = shape
}
} else {
var shapes = [Shape]()
try pattern.children.forEach { indexer in
if let shape = try parseNode(indexer) as? Shape {
shapes.append(shape)
}
}
contentNode = Group(contents: shapes)
}
return UserSpacePattern(content: contentNode!, bounds: bounds, userSpace: userSpace, contentUserSpace: contentUserSpace)
}
fileprivate func parseGroup(_ group: XMLIndexer, style: [String: String]) throws -> Group? {
guard let element = group.element else {
return .none
@ -563,7 +615,7 @@ open class SVGParser {
return createColorFromHex(colorString, opacity: opacity)
}
fileprivate func getFillColor(_ styleParts: [String: String], groupStyle: [String: String] = [:]) -> Fill? {
fileprivate func getFillColor(_ styleParts: [String: String], groupStyle: [String: String] = [:], locus: Locus? = nil) -> Fill? {
var opacity: Double = 1
if let fillOpacity = styleParts["fill-opacity"] {
opacity = Double(fillOpacity.replacingOccurrences(of: " ", with: "")) ?? 1
@ -573,7 +625,11 @@ open class SVGParser {
return Color.black.with(a: opacity)
}
if let colorId = parseIdFromUrl(fillColor) {
return defFills[colorId]
let fill = defFills[colorId]
if let pattern = fill as? UserSpacePattern {
return getPatternFill(pattern: pattern, locus: locus)
}
return fill
}
if fillColor == "currentColor", let currentColor = groupStyle["color"] {
fillColor = currentColor
@ -582,6 +638,18 @@ open class SVGParser {
return createColor(fillColor.replacingOccurrences(of: " ", with: ""), opacity: opacity)
}
fileprivate func getPatternFill(pattern: UserSpacePattern, locus: Locus?) -> Pattern {
if pattern.userSpace == false && pattern.contentUserSpace == true {
let tranform = BoundsUtils.transformForLocusInRespectiveCoords(respectiveLocus: pattern.bounds, absoluteLocus: locus!)
return Pattern(content: pattern.content, bounds: pattern.bounds.applying(tranform), userSpace: true)
}
if pattern.userSpace == true && pattern.contentUserSpace == false {
BoundsUtils.applyTransformToNodeInRespectiveCoords(respectiveNode: pattern.content, absoluteLocus: locus!)
return Pattern(content: pattern.content, bounds: pattern.bounds, userSpace: pattern.userSpace)
}
return Pattern(content: pattern.content, bounds: pattern.bounds, userSpace: true)
}
fileprivate func getStroke(_ styleParts: [String: String], groupStyle: [String: String] = [:]) -> Stroke? {
guard var strokeColor = styleParts["stroke"] else {
return .none
@ -1410,15 +1478,6 @@ open class SVGParser {
return false
}
fileprivate func transformBoundingBoxLocus(respectiveLocus: Locus, absoluteLocus: Locus) -> Transform {
let absoluteBounds = absoluteLocus.bounds()
let respectiveBounds = respectiveLocus.bounds()
let finalSize = Size(w: absoluteBounds.w * respectiveBounds.w,
h: absoluteBounds.h * respectiveBounds.h)
let scale = ContentLayout.of(contentMode: .scaleToFill).layout(size: respectiveBounds.size(), into: finalSize)
return Transform.move(dx: absoluteBounds.x, dy: absoluteBounds.y).concat(with: scale)
}
fileprivate func getClipPath(_ attributes: [String: String], locus: Locus?) -> Locus? {
if let clipPath = attributes["clip-path"], let id = parseIdFromUrl(clipPath) {
if let userSpaceLocus = defClip[id] {
@ -1426,7 +1485,7 @@ open class SVGParser {
guard let locus = locus else {
return .none
}
let transform = transformBoundingBoxLocus(respectiveLocus: userSpaceLocus.locus, absoluteLocus: locus)
let transform = BoundsUtils.transformForLocusInRespectiveCoords(respectiveLocus: userSpaceLocus.locus, absoluteLocus: locus)
return TransformedLocus(locus: userSpaceLocus.locus, transform: transform)
}
return userSpaceLocus.locus
@ -1443,13 +1502,13 @@ open class SVGParser {
if let group = userSpaceNode.node as? Group {
for node in group.contents {
if let shape = node as? Shape {
shape.place = transformBoundingBoxLocus(respectiveLocus: shape.form, absoluteLocus: locus)
shape.place = BoundsUtils.transformForLocusInRespectiveCoords(respectiveLocus: shape.form, absoluteLocus: locus)
}
}
return group
}
if let shape = userSpaceNode.node as? Shape {
shape.place = transformBoundingBoxLocus(respectiveLocus: shape.form, absoluteLocus: locus)
shape.place = BoundsUtils.transformForLocusInRespectiveCoords(respectiveLocus: shape.form, absoluteLocus: locus)
return shape
} else {
throw SVGParserError.maskUnsupportedNodeType
@ -1759,3 +1818,18 @@ fileprivate class UserSpaceNode {
self.userSpace = userSpace
}
}
fileprivate class UserSpacePattern: Fill {
public let content: Node
public let bounds: Rect
public let userSpace: Bool
public let contentUserSpace: Bool
init(content: Node, bounds: Rect, userSpace: Bool = false, contentUserSpace: Bool = true) {
self.content = content
self.bounds = bounds
self.userSpace = userSpace
self.contentUserSpace = contentUserSpace
}
}

View File

@ -52,4 +52,25 @@ final internal class BoundsUtils {
}
return union
}
class func transformForLocusInRespectiveCoords(respectiveLocus: Locus, absoluteLocus: Locus) -> Transform {
let absoluteBounds = absoluteLocus.bounds()
let respectiveBounds = respectiveLocus.bounds()
let finalSize = Size(w: absoluteBounds.w * respectiveBounds.w,
h: absoluteBounds.h * respectiveBounds.h)
let scale = ContentLayout.of(contentMode: .scaleToFill).layout(size: respectiveBounds.size(), into: finalSize)
return Transform.move(dx: absoluteBounds.x, dy: absoluteBounds.y).concat(with: scale)
}
class func applyTransformToNodeInRespectiveCoords(respectiveNode: Node, absoluteLocus: Locus) {
if let patternShape = respectiveNode as? Shape {
let tranform = BoundsUtils.transformForLocusInRespectiveCoords(respectiveLocus: patternShape.form, absoluteLocus: absoluteLocus)
patternShape.place = patternShape.place.concat(with: tranform)
}
if let patternGroup = respectiveNode as? Group {
for groupNode in patternGroup.contents {
applyTransformToNodeInRespectiveCoords(respectiveNode: groupNode, absoluteLocus: absoluteLocus)
}
}
}
}