diff --git a/Macaw.xcodeproj/project.pbxproj b/Macaw.xcodeproj/project.pbxproj index b6a5bf7c..8779046e 100644 --- a/Macaw.xcodeproj/project.pbxproj +++ b/Macaw.xcodeproj/project.pbxproj @@ -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 = ""; }; 5B7E79DC20D2781A00C50BCF /* masking-intro-01-f-manual.reference */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "masking-intro-01-f-manual.reference"; sourceTree = ""; }; 5B7E79DD20D2781A00C50BCF /* masking-intro-01-f-manual.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "masking-intro-01-f-manual.svg"; sourceTree = ""; }; + 5B7E79BF20CA7E9300C50BCF /* Pattern.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pattern.swift; sourceTree = ""; }; + 5B7E79C220CA7F1A00C50BCF /* pservers-grad-03-b-manual.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "pservers-grad-03-b-manual.svg"; sourceTree = ""; }; + 5B7E79C320CA7F1B00C50BCF /* pservers-grad-03-b-manual.reference */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "pservers-grad-03-b-manual.reference"; sourceTree = ""; }; 5BAE201E208E1211006BF277 /* SVGCanvas.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGCanvas.swift; sourceTree = ""; }; 5BAE2022208E1637006BF277 /* polyline.reference */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = polyline.reference; sourceTree = ""; }; 5BAE2023208E1637006BF277 /* polygon.reference */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = polygon.reference; sourceTree = ""; }; @@ -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 */, diff --git a/MacawTests/MacawSVGTests.swift b/MacawTests/MacawSVGTests.swift index 672d306d..9b210759 100644 --- a/MacawTests/MacawSVGTests.swift +++ b/MacawTests/MacawSVGTests.swift @@ -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") + } } diff --git a/MacawTests/w3c-test-suite.md b/MacawTests/w3c-test-suite.md index 8787cb96..f70a0bcc 100644 --- a/MacawTests/w3c-test-suite.md +++ b/MacawTests/w3c-test-suite.md @@ -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) | diff --git a/MacawTests/w3cSVGTests/pservers-grad-03-b-manual.reference b/MacawTests/w3cSVGTests/pservers-grad-03-b-manual.reference new file mode 100644 index 00000000..0bc35801 --- /dev/null +++ b/MacawTests/w3cSVGTests/pservers-grad-03-b-manual.reference @@ -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" +} \ No newline at end of file diff --git a/MacawTests/w3cSVGTests/pservers-grad-03-b-manual.svg b/MacawTests/w3cSVGTests/pservers-grad-03-b-manual.svg new file mode 100755 index 00000000..aa33de24 --- /dev/null +++ b/MacawTests/w3cSVGTests/pservers-grad-03-b-manual.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + +

+ Test that the viewer can handle the xlink:href attribute on + patterns. +

+

+ 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. +

+ + +

+ Run the test. No interaction required. +

+
+ +

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.

+
+ + $RCSfile: pservers-grad-03-b.svg,v $ + + + + + + + + + + + + + + + + + + + + Pattern fill. + + Referencing pattern fill below. + + + $Revision: 1.8 $ + + + + + diff --git a/Source/model/draw/Pattern.swift b/Source/model/draw/Pattern.swift new file mode 100644 index 00000000..d0a1ff8f --- /dev/null +++ b/Source/model/draw/Pattern.swift @@ -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 + } +} diff --git a/Source/render/ShapeRenderer.swift b/Source/render/ShapeRenderer.swift index 441b8763..27ebbbbf 100644 --- a/Source/render/ShapeRenderer.swift +++ b/Source/render/ShapeRenderer.swift @@ -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] = [] diff --git a/Source/svg/SVGParser.swift b/Source/svg/SVGParser.swift index 0feae180..40f21abe 100644 --- a/Source/svg/SVGParser.swift +++ b/Source/svg/SVGParser.swift @@ -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 + } +} diff --git a/Source/utils/BoundsUtils.swift b/Source/utils/BoundsUtils.swift index bab7b9db..fee1790d 100644 --- a/Source/utils/BoundsUtils.swift +++ b/Source/utils/BoundsUtils.swift @@ -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) + } + } + } }