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:
parent
a3a9ff5c5d
commit
0d2bd69d75
@ -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 */,
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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) |
|
||||
|
121
MacawTests/w3cSVGTests/pservers-grad-03-b-manual.reference
Normal file
121
MacawTests/w3cSVGTests/pservers-grad-03-b-manual.reference
Normal 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"
|
||||
}
|
77
MacawTests/w3cSVGTests/pservers-grad-03-b-manual.svg
Executable file
77
MacawTests/w3cSVGTests/pservers-grad-03-b-manual.svg
Executable 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 |
12
Source/model/draw/Pattern.swift
Normal file
12
Source/model/draw/Pattern.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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] = []
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user