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

Merge pull request #526 from f3dm76/task/pattern-fill

Task/pattern fill
This commit is contained in:
Yuri Strot 2019-02-12 12:25:53 +07:00 committed by GitHub
commit 95a01d8657
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 445 additions and 29 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

@ -31,4 +31,32 @@ class SceneUtils {
return shape
}
static func copyNode(_ referenceNode: Node) -> Node? {
let pos = referenceNode.place
let opaque = referenceNode.opaque
let visible = referenceNode.visible
let clip = referenceNode.clip
let tag = referenceNode.tag
if let shape = referenceNode as? Shape {
return Shape(form: shape.form, fill: shape.fill, stroke: shape.stroke, place: pos, opaque: opaque, clip: clip, visible: visible, tag: tag)
}
if let text = referenceNode as? Text {
return Text(text: text.text, font: text.font, fill: text.fill, stroke: text.stroke, align: text.align, baseline: text.baseline, place: pos, opaque: opaque, clip: clip, visible: visible, tag: tag)
}
if let image = referenceNode as? Image {
return Image(src: image.src, xAlign: image.xAlign, yAlign: image.yAlign, aspectRatio: image.aspectRatio, w: image.w, h: image.h, place: pos, opaque: opaque, clip: clip, visible: visible, tag: tag)
}
if let group = referenceNode as? Group {
var contents = [Node]()
group.contents.forEach { node in
if let copy = copyNode(node) {
contents.append(copy)
}
}
return Group(contents: contents, place: pos, opaque: opaque, clip: clip, visible: visible, tag: tag)
}
return .none
}
}

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,26 @@ class ShapeRenderer: NodeRenderer {
drawGradient(gradient, ctx: ctx, opacity: opacity)
}
fileprivate func drawPattern(_ pattern: Pattern, ctx: CGContext?, opacity: Double) {
guard let shape = shape else {
return
}
var patternNode = pattern.content
if !pattern.userSpace, let node = BoundsUtils.createNodeFromRespectiveCoords(respectiveNode: pattern.content, absoluteLocus: shape.form) {
patternNode = node
}
let renderer = RenderUtils.createNodeRenderer(patternNode, 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

@ -92,6 +92,7 @@ open class SVGParser {
fileprivate var defMasks = [String: UserSpaceNode]()
fileprivate var defClip = [String: UserSpaceLocus]()
fileprivate var defEffects = [String: Effect]()
fileprivate var defPatterns = [String: UserSpacePattern]()
fileprivate var styles = CSSParser()
@ -155,7 +156,9 @@ open class SVGParser {
if let id = element.allAttributes["id"]?.text {
switch element.name {
case "linearGradient", "radialGradient", "fill":
defFills[id] = parseFill(node)
defFills[id] = try parseFill(node)
case "pattern":
defPatterns[id] = try parsePattern(node)
case "mask":
defMasks[id] = try parseMask(node)
case "filter":
@ -272,37 +275,37 @@ 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 +325,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
}
@ -337,6 +340,54 @@ open class SVGParser {
}
}
fileprivate func parsePattern(_ pattern: XMLIndexer) throws -> UserSpacePattern? {
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 = defPatterns[id]
}
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 +614,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 +624,12 @@ open class SVGParser {
return Color.black.with(a: opacity)
}
if let colorId = parseIdFromUrl(fillColor) {
return defFills[colorId]
if let fill = defFills[colorId] {
return fill
}
if let pattern = defPatterns[colorId] {
return getPatternFill(pattern: pattern, locus: locus)
}
}
if fillColor == "currentColor", let currentColor = groupStyle["color"] {
fillColor = currentColor
@ -582,6 +638,19 @@ 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 {
if let patternNode = BoundsUtils.createNodeFromRespectiveCoords(respectiveNode: pattern.content, absoluteLocus: locus!) {
return Pattern(content: patternNode, 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 +1479,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 +1486,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 +1503,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 +1819,17 @@ fileprivate class UserSpaceNode {
self.userSpace = userSpace
}
}
fileprivate class UserSpacePattern {
let content: Node
let bounds: Rect
let userSpace: Bool
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,35 @@ 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 createNodeFromRespectiveCoords(respectiveNode: Node, absoluteLocus: Locus) -> Node? {
guard let copy = SceneUtils.copyNode(respectiveNode) else {
return nil
}
if let patternShape = copy as? Shape {
let tranform = BoundsUtils.transformForLocusInRespectiveCoords(respectiveLocus: patternShape.form, absoluteLocus: absoluteLocus)
patternShape.place = patternShape.place.concat(with: tranform)
}
if let patternGroup = copy as? Group {
var nodes = [Node]()
for groupNode in patternGroup.contents {
if let node = createNodeFromRespectiveCoords(respectiveNode: groupNode, absoluteLocus: absoluteLocus) {
nodes.append(node)
}
}
patternGroup.contents = nodes
}
return copy
}
}