Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 22 additions & 26 deletions Examples/Sources/GalleryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,37 +34,33 @@ import SwiftUI

struct GalleryView: View {

var images: [SVG] = {
[
"thats-no-moon.svg",
"avocado.svg",
"angry.svg",
"ogre.svg",
"monkey.svg",
"fuji.svg",
"dish.svg",
"mouth-open.svg",
"sleepy.svg",
"smile.svg",
"snake.svg",
"spider.svg",
"star-struck.svg",
"worried.svg",
"yawning.svg",
"thats-no-moon.svg",
"alert.svg",
"effigy.svg",
"stylesheet-multiple.svg"
].compactMap {
SVG(named: $0, in: .samples)
}
}()
var images = [
"thats-no-moon.svg",
"avocado.svg",
"angry.svg",
"ogre.svg",
"monkey.svg",
"fuji.svg",
"dish.svg",
"mouth-open.svg",
"sleepy.svg",
"smile.svg",
"snake.svg",
"spider.svg",
"star-struck.svg",
"worried.svg",
"yawning.svg",
"thats-no-moon.svg",
"alert.svg",
"effigy.svg",
"stylesheet-multiple.svg"
]

var body: some View {
ScrollView {
LazyVStack(spacing: 20) {
ForEach(images, id: \.self) { image in
SVGView(svg: image)
SVGView(image, bundle: .samples)
.aspectRatio(contentMode: .fit)
.padding([.leading, .trailing], 10)
}
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,14 @@ Display an image within `SVGView`:

```swift
var body: some View {
SVGView(named: "sample.svg")
SVGView("sample.svg")
.aspectRatio(contentMode: .fit)
.padding()
}
```

Pass an `SVG` instance for better performance:
When you load by name, SVGView uses an internal cache so repeated lookups are efficient.
For more predictable performance (avoiding any cache lookup or parsing), you can pass an already-created SVG instance:

```swift
var image: SVG
Expand Down
41 changes: 35 additions & 6 deletions SwiftDraw/Sources/LayerTree/LayerTree.CommandGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ extension LayerTree {
private var hasLoggedGradientWarning = false
private var hasLoggedMaskWarning = false

private var paths: [LayerTree.Shape: P.Types.Path] = [:]
private var images: [LayerTree.Image: P.Types.Image] = [:]

init(provider: P, size: LayerTree.Size, scale: LayerTree.Float = 3.0, options: SVG.Options) {
self.provider = provider
self.size = size
Expand Down Expand Up @@ -192,7 +195,7 @@ extension LayerTree {
fill: FillAttributes,
colorConverter: any ColorConverter) -> [RendererCommand<P.Types>] {
var commands = [RendererCommand<P.Types>]()
let path = provider.createPath(from: shape)
let path = makeCachedPath(from: shape)

switch fill.fill {
case .color(let color):
Expand Down Expand Up @@ -303,7 +306,7 @@ extension LayerTree {
}

func renderCommands(for image: Image) -> [RendererCommand<P.Types>] {
guard let renderImage = provider.createImage(from: image) else { return [] }
guard let renderImage = makeCachedImage(from: image) else { return [] }
let size = provider.createSize(from: renderImage)
guard size.width > 0 && size.height > 0 else { return [] }

Expand All @@ -312,6 +315,26 @@ extension LayerTree {
return [.draw(image: renderImage, in: rect)]
}

private func makeCachedPath(from shape: LayerTree.Shape) -> P.Types.Path {
if let existing = paths[shape] {
return existing
}
let new = provider.createPath(from: shape)
paths[shape] = new
return new
}

private func makeCachedImage(from image: Image) -> P.Types.Image? {
if let existing = images[image] {
return existing
}
guard let new = provider.createImage(from: image) else {
return nil
}
images[image] = new
return new
}

func makeImageFrame(for image: Image, bitmapSize: LayerTree.Size) -> LayerTree.Rect {
var frame = LayerTree.Rect(
x: image.origin.x,
Expand Down Expand Up @@ -376,14 +399,20 @@ extension LayerTree {
guard !shapes.isEmpty else { return [] }
let paths = shapes.map { clip in
if clip.transform == .identity {
return provider.createPath(from: clip.shape)
return makeCachedPath(from: clip.shape)
} else {
return provider.createPath(from: .path(clip.shape.path.applying(matrix: clip.transform)))
return makeCachedPath(from: .path(clip.shape.path.applying(matrix: clip.transform)))
}
}
let clipPath = provider.createPath(from: paths)

let rule = provider.createFillRule(from: rule ?? .nonzero)
return [.setClip(path: clipPath, rule: rule)]

if paths.count == 1 {
return [.setClip(path: paths[0], rule: rule)]
} else {
let clipPath = provider.createPath(from: paths)
return [.setClip(path: clipPath, rule: rule)]
}
}


Expand Down
119 changes: 119 additions & 0 deletions SwiftDraw/Sources/Renderer/Renderer.CoreGraphics+Cost.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//
// Renderer.CoreGraphics+Cost.swift
// SwiftDraw
//
// Created by Simon Whitty on 31/8/25.
// Copyright 2025 WhileLoop Pty Ltd. All rights reserved.
//
// Distributed under the permissive zlib license
// Get the latest version from here:
//
// https://github.com/swhitty/SwiftDraw
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source distribution.
//

#if canImport(CoreGraphics)
import CoreGraphics

extension [RendererCommand<CGTypes>] {

var estimatedCost: Int {
let commandCost = MemoryLayout<Self>.stride * count
let pathCost = Set(allPaths).reduce(0) { $0 + $1.estimatedCost }
let imageCost = Set(allImages).reduce(0) { $0 + $1.estimatedCost }
return commandCost + pathCost + imageCost
}
}

extension CGPath {

var estimatedCost: Int {
var total = 0
applyWithBlock { element in
switch element.pointee.type {
case .moveToPoint, .addLineToPoint, .closeSubpath:
total += MemoryLayout<CGPathElement>.stride + MemoryLayout<CGPoint>.stride
case .addQuadCurveToPoint:
total += MemoryLayout<CGPathElement>.stride + 2 * MemoryLayout<CGPoint>.stride
case .addCurveToPoint:
total += MemoryLayout<CGPathElement>.stride + 3 * MemoryLayout<CGPoint>.stride
@unknown default:
break
}
}
return MemoryLayout<CGPath>.size + total
}
}

extension CGImage {
var estimatedCost: Int { bytesPerRow * height }
}

extension RendererCommand<CGTypes> {

var allPaths: [CGPath] {
switch self {
case .setClip(path: let p, rule: _):
return [p]
case .setFillPattern(let p):
return p.contents.allPaths
case .stroke(let p):
return [p]
case .clipStrokeOutline(let p):
return [p]
case .fill(let p, rule: _):
return [p]
default:
return []
}
}

var allImages: [CGImage] {
switch self {
case .setFillPattern(let p):
return p.contents.allImages
case .draw(image: let i, in: _):
return [i]
default:
return []
}
}
}

extension [RendererCommand<CGTypes>] {

var allPaths: [CGPath] {
var paths = [CGPath]()
for command in self {
paths.append(contentsOf: command.allPaths)
}
return paths
}

var allImages: [CGImage] {
var images = [CGImage]()
for command in self {
images.append(contentsOf: command.allImages)
}
return images
}
}

#endif
4 changes: 2 additions & 2 deletions SwiftDraw/Sources/Renderer/Renderer.CoreGraphics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ struct CGTypes: RendererTypes, Sendable {

struct CGTransformingPattern: Hashable {

let bounds: CGRect
let contents: [RendererCommand<CGTypes>]
var bounds: CGRect
var contents: [RendererCommand<CGTypes>]

init(bounds: CGRect, contents: [RendererCommand<CGTypes>]) {
self.bounds = bounds
Expand Down
17 changes: 11 additions & 6 deletions SwiftDraw/Sources/SVG.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,17 @@ public struct SVG: Hashable, Sendable {
var commands: [RendererCommand<CGTypes>]

public init?(fileURL url: URL, options: SVG.Options = .default) {
do {
let svg = try DOM.SVG.parse(fileURL: url)
self.init(dom: svg, options: options)
} catch {
XMLParser.logParsingError(for: error, filename: url.lastPathComponent, parsing: nil)
return nil
if let svg = SVGGCache.shared.svg(fileURL: url) {
self = svg
} else {
do {
let svg = try DOM.SVG.parse(fileURL: url)
self.init(dom: svg, options: options)
SVGGCache.shared.setSVG(self, for: url)
} catch {
XMLParser.logParsingError(for: error, filename: url.lastPathComponent, parsing: nil)
return nil
}
}
}

Expand Down
75 changes: 75 additions & 0 deletions SwiftDraw/Sources/SVGCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// SVG.swift
// SwiftDraw
//
// Created by Simon Whitty on 24/5/17.
// Copyright 2020 Simon Whitty
//
// Distributed under the permissive zlib license
// Get the latest version from here:
//
// https://github.com/swhitty/SwiftDraw
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source distribution.
//

import SwiftDrawDOM
public import Foundation

#if canImport(CoreGraphics)
import CoreGraphics

public final class SVGGCache: Sendable {

public static let shared = SVGGCache()

nonisolated(unsafe)private let cache: NSCache<NSURL, Box<SVG>>

public init(totalCostLimit: Int = defaultTotalCostLimit) {
self.cache = NSCache()
self.cache.totalCostLimit = totalCostLimit
}

public func svg(fileURL: URL) -> SVG? {
cache.object(forKey: fileURL as NSURL)?.value
}

public func setSVG(_ svg: SVG, for fileURL: URL) {
cache.setObject(Box(svg), forKey: fileURL as NSURL, cost: svg.commands.estimatedCost)
}

final class Box<T: Hashable>: NSObject {
let value: T
init(_ value: T) { self.value = value }
}

public static var defaultTotalCostLimit: Int {
#if canImport(WatchKit)
// 2 MB
return 2 * 1024 * 1024
#elseif canImport(AppKit)
// 200 MB
return 200 * 1024 * 1024
#else
// 50 MB
return 50 * 1024 * 1024
#endif
}
}
#endif
Loading