From b6f681e1521488dc7c918fd54b1d80974e7205fb Mon Sep 17 00:00:00 2001 From: Simon Whitty Date: Sun, 23 Feb 2025 16:32:16 +1100 Subject: [PATCH 1/6] scale SVG --- Examples/Sources/GalleryView.swift | 2 +- SwiftDraw/SVG.swift | 37 ++++++++++++++++++++++++++---- SwiftDrawTests/SVGTests.swift | 13 +++++++++++ 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/Examples/Sources/GalleryView.swift b/Examples/Sources/GalleryView.swift index 846a0340..a59c2ba7 100644 --- a/Examples/Sources/GalleryView.swift +++ b/Examples/Sources/GalleryView.swift @@ -58,7 +58,7 @@ struct GalleryView: View { ScrollView { LazyVStack(spacing: 20) { ForEach(images, id: \.self) { image in - SVGView(svg: image) + SVGView(svg: image.scale(x: 3, y: 1)) .aspectRatio(contentMode: .fit) .padding([.leading, .trailing], 10) } diff --git a/SwiftDraw/SVG.swift b/SwiftDraw/SVG.swift index 6d8605f3..6ce67602 100644 --- a/SwiftDraw/SVG.swift +++ b/SwiftDraw/SVG.swift @@ -35,11 +35,11 @@ import Foundation import CoreGraphics public struct SVG: Hashable { - public let size: CGSize + public private(set) var size: CGSize // Array of commands that render the image // see: Renderer.swift - let commands: [RendererCommand] + var commands: [RendererCommand] public init?(fileURL url: URL, options: SVG.Options = .default) { do { @@ -78,8 +78,34 @@ public struct SVG: Hashable { } } -@available(*, unavailable, renamed: "SVG") -public enum Image { } +extension SVG { + + public func scale(_ factor: CGFloat) -> SVG { + scale(x: factor, y: factor) + } + + public func scale(x: CGFloat, y: CGFloat) -> SVG { + var copy = self + + copy.commands.insert(.scale(sx: x, sy: y), at: 0) + copy.size = CGSize( + width: size.width * x, + height: size.height * y + ) + return copy + } +} + +extension SVG { + + public mutating func scaled(_ factor: CGFloat) { + self = scale(factor) + } + + public mutating func scaled(x: CGFloat, y: CGFloat) { + self = scale(x: x, y: y) + } +} extension SVG { @@ -103,6 +129,9 @@ extension SVG { } } +@available(*, unavailable, renamed: "SVG") +public enum Image { } + #else public struct SVG { diff --git a/SwiftDrawTests/SVGTests.swift b/SwiftDrawTests/SVGTests.swift index 7d99468c..c88e300e 100644 --- a/SwiftDrawTests/SVGTests.swift +++ b/SwiftDrawTests/SVGTests.swift @@ -85,6 +85,19 @@ final class SVGTests: XCTestCase { XCTAssertEqual(reloaded.scale, 1) } #endif + + func testScale() { + let image = SVG.makeLines() + + XCTAssertEqual(image.size, CGSize(width: 100, height: 100)) + XCTAssertEqual(image.scale(2).size, CGSize(width: 200, height: 200)) + XCTAssertEqual(image.scale(0.5).size, CGSize(width: 50, height: 50)) + XCTAssertEqual(image.scale(x: 2, y: 3).size, CGSize(width: 200, height: 300)) + + var copy = image + copy.scaled(5) + XCTAssertEqual(copy.size, CGSize(width: 500, height: 500)) + } } private extension SVG { From ab0c50c7a660d1511fcd4c880dee9760daae1f09 Mon Sep 17 00:00:00 2001 From: Simon Whitty Date: Sun, 23 Feb 2025 18:22:55 +1100 Subject: [PATCH 2/6] translate and expand --- SwiftDraw/CommandLine+Process.swift | 21 ++++++++++++----- SwiftDraw/SVG+CoreGraphics.swift | 22 +++++++----------- SwiftDraw/SVG.swift | 36 +++++++++++++++++++++++++++++ SwiftDrawTests/SVGTests.swift | 22 ++++++++++++++++++ 4 files changed, 81 insertions(+), 20 deletions(-) diff --git a/SwiftDraw/CommandLine+Process.swift b/SwiftDraw/CommandLine+Process.swift index 478dcea7..a305b3dc 100644 --- a/SwiftDraw/CommandLine+Process.swift +++ b/SwiftDraw/CommandLine+Process.swift @@ -95,14 +95,17 @@ public extension CommandLine { #if canImport(CoreGraphics) switch config.format { case .jpeg: - let insets = try makeImageInsets(for: config.insets) - return try image.jpegData(size: config.size.cgValue, scale: config.scale.cgValue, insets: insets) + return try image + .inset(makeImageInsets(for: config.insets)) + .jpegData(size: config.size.cgValue, scale: config.scale.cgValue) case .pdf: - let insets = try makeImageInsets(for: config.insets) - return try image.pdfData(size: config.size.cgValue, insets: insets) + return try image + .inset(makeImageInsets(for: config.insets)) + .pdfData(size: config.size.cgValue) case .png: - let insets = try makeImageInsets(for: config.insets) - return try image.pngData(size: config.size.cgValue, scale: config.scale.cgValue, insets: insets) + return try image + .inset(makeImageInsets(for: config.insets)) + .pngData(size: config.size.cgValue, scale: config.scale.cgValue) case .swift, .sfsymbol: throw Error.unsupported } @@ -129,6 +132,12 @@ public extension CommandLine { } } +private extension SVG { + func inset(_ insets: Insets) -> SVG { + expand(top: -insets.top, left: -insets.left, bottom: -insets.bottom, right: -insets.right) + } +} + #if canImport(CoreGraphics) private extension CommandLine.Scale { var cgValue: CGFloat { diff --git a/SwiftDraw/SVG+CoreGraphics.swift b/SwiftDraw/SVG+CoreGraphics.swift index a0b53a90..785cc5cc 100644 --- a/SwiftDraw/SVG+CoreGraphics.swift +++ b/SwiftDraw/SVG+CoreGraphics.swift @@ -38,23 +38,17 @@ public extension CGContext { func draw(_ image: SVG, in rect: CGRect? = nil) { let defaultRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height) let renderer = CGRenderer(context: self) + saveGState() - guard let rect = rect, rect != defaultRect else { - renderer.perform(image.commands) - return + if let rect = rect, rect != defaultRect { + translateBy(x: rect.origin.x, y: rect.origin.y) + scaleBy( + x: rect.width / image.size.width, + y: rect.height / image.size.height + ) } + renderer.perform(image.commands) - let scale = CGSize(width: rect.width / image.size.width, - height: rect.height / image.size.height) - draw(image.commands, in: rect, scale: scale) - } - - fileprivate func draw(_ commands: [RendererCommand], in rect: CGRect, scale: CGSize = CGSize(width: 1.0, height: 1.0)) { - let renderer = CGRenderer(context: self) - saveGState() - translateBy(x: rect.origin.x, y: rect.origin.y) - scaleBy(x: scale.width, y: scale.height) - renderer.perform(commands) restoreGState() } } diff --git a/SwiftDraw/SVG.swift b/SwiftDraw/SVG.swift index 6ce67602..02ad8af4 100644 --- a/SwiftDraw/SVG.swift +++ b/SwiftDraw/SVG.swift @@ -94,6 +94,27 @@ extension SVG { ) return copy } + + public func translate(tx: CGFloat, ty: CGFloat) -> SVG { + var copy = self + copy.commands.insert(.translate(tx: tx, ty: ty), at: 0) + return copy + } + + public func expand(_ padding: CGFloat) -> SVG { + expand(top: padding, left: padding, bottom: padding, right: padding) + } + + public func expand(top: CGFloat = 0, + left: CGFloat = 0, + bottom: CGFloat = 0, + right: CGFloat = 0) -> SVG { + var copy = self + copy.commands.insert(.translate(tx: left, ty: top), at: 0) + copy.size.width += left + right + copy.size.height += top + bottom + return copy + } } extension SVG { @@ -105,6 +126,21 @@ extension SVG { public mutating func scaled(x: CGFloat, y: CGFloat) { self = scale(x: x, y: y) } + + public mutating func translated(tx: CGFloat, ty: CGFloat) { + self = translate(tx: tx, ty: ty) + } + + public mutating func expanded(_ padding: CGFloat) { + self = expand(padding) + } + + public mutating func expanded(top: CGFloat = 0, + left: CGFloat = 0, + bottom: CGFloat = 0, + right: CGFloat = 0) { + self = expand(top: top, left: left, bottom: bottom, right: right) + } } extension SVG { diff --git a/SwiftDrawTests/SVGTests.swift b/SwiftDrawTests/SVGTests.swift index c88e300e..c334d6b8 100644 --- a/SwiftDrawTests/SVGTests.swift +++ b/SwiftDrawTests/SVGTests.swift @@ -98,6 +98,28 @@ final class SVGTests: XCTestCase { copy.scaled(5) XCTAssertEqual(copy.size, CGSize(width: 500, height: 500)) } + + func testTranslate() { + let image = SVG.makeLines() + + XCTAssertEqual(image.size, CGSize(width: 100, height: 100)) + XCTAssertEqual(image.translate(tx: 10, ty: 10).size, CGSize(width: 100, height: 100)) + + var copy = image + copy.translated(tx: 50, ty: 50) + XCTAssertEqual(copy.size, CGSize(width: 100, height: 100)) + } + + func testExpand() { + let image = SVG.makeLines() + + XCTAssertEqual(image.size, CGSize(width: 100, height: 100)) + XCTAssertEqual(image.expand(top: 50, right: 30).size, CGSize(width: 130, height: 150)) + + var copy = image + copy.expanded(-10) + XCTAssertEqual(copy.size, CGSize(width: 80, height: 80)) + } } private extension SVG { From 5177eeff3ee0e44754c8a50233e6ba9f1a1d2d69 Mon Sep 17 00:00:00 2001 From: Simon Whitty Date: Sun, 23 Feb 2025 18:41:43 +1100 Subject: [PATCH 3/6] size --- SwiftDraw/CommandLine+Process.swift | 15 ++++++++++++--- SwiftDraw/SVG.swift | 16 ++++++++++++++++ SwiftDrawTests/SVGTests.swift | 11 +++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/SwiftDraw/CommandLine+Process.swift b/SwiftDraw/CommandLine+Process.swift index a305b3dc..08d68327 100644 --- a/SwiftDraw/CommandLine+Process.swift +++ b/SwiftDraw/CommandLine+Process.swift @@ -97,15 +97,18 @@ public extension CommandLine { case .jpeg: return try image .inset(makeImageInsets(for: config.insets)) - .jpegData(size: config.size.cgValue, scale: config.scale.cgValue) + .size(config.size.cgValue) + .jpegData(scale: config.scale.cgValue) case .pdf: return try image .inset(makeImageInsets(for: config.insets)) - .pdfData(size: config.size.cgValue) + .size(config.size.cgValue) + .pdfData() case .png: return try image .inset(makeImageInsets(for: config.insets)) - .pngData(size: config.size.cgValue, scale: config.scale.cgValue) + .size(config.size.cgValue) + .pngData(scale: config.scale.cgValue) case .swift, .sfsymbol: throw Error.unsupported } @@ -133,6 +136,12 @@ public extension CommandLine { } private extension SVG { + + func size(_ s: CGSize?) -> SVG { + guard let s else { return self } + return size(s) + } + func inset(_ insets: Insets) -> SVG { expand(top: -insets.top, left: -insets.left, bottom: -insets.bottom, right: -insets.right) } diff --git a/SwiftDraw/SVG.swift b/SwiftDraw/SVG.swift index 02ad8af4..a55edf98 100644 --- a/SwiftDraw/SVG.swift +++ b/SwiftDraw/SVG.swift @@ -80,6 +80,18 @@ public struct SVG: Hashable { extension SVG { + public func size(_ s: CGSize) -> SVG { + guard size != s else { return self } + + let sx = s.width / size.width + let sy = s.height / size.height + + var copy = self + copy.commands.insert(.scale(sx: sx, sy: sy), at: 0) + copy.size = s + return copy + } + public func scale(_ factor: CGFloat) -> SVG { scale(x: factor, y: factor) } @@ -119,6 +131,10 @@ extension SVG { extension SVG { + public mutating func sized(_ s: CGSize) { + self = size(s) + } + public mutating func scaled(_ factor: CGFloat) { self = scale(factor) } diff --git a/SwiftDrawTests/SVGTests.swift b/SwiftDrawTests/SVGTests.swift index c334d6b8..6349808d 100644 --- a/SwiftDrawTests/SVGTests.swift +++ b/SwiftDrawTests/SVGTests.swift @@ -86,6 +86,17 @@ final class SVGTests: XCTestCase { } #endif + func testSize() { + let image = SVG.makeLines() + + XCTAssertEqual(image.size, CGSize(width: 100, height: 100)) + XCTAssertEqual(image.size(CGSize(width: 200, height: 200)).size, CGSize(width: 200, height: 200)) + + var copy = image + copy.sized(CGSize(width: 20, height: 20)) + XCTAssertEqual(copy.size, CGSize(width: 20, height: 20)) + } + func testScale() { let image = SVG.makeLines() From a43bfea9c22cdafabbcab31a7f6afc923661dae5 Mon Sep 17 00:00:00 2001 From: Simon Whitty Date: Sun, 23 Feb 2025 19:30:21 +1100 Subject: [PATCH 4/6] linux transformation stubs --- SwiftDraw/SVG.swift | 95 ++++++++++++++++++++++++++------------------- 1 file changed, 55 insertions(+), 40 deletions(-) diff --git a/SwiftDraw/SVG.swift b/SwiftDraw/SVG.swift index a55edf98..92ffccd9 100644 --- a/SwiftDraw/SVG.swift +++ b/SwiftDraw/SVG.swift @@ -78,9 +78,9 @@ public struct SVG: Hashable { } } -extension SVG { +public extension SVG { - public func size(_ s: CGSize) -> SVG { + func size(_ s: CGSize) -> SVG { guard size != s else { return self } let sx = s.width / size.width @@ -92,11 +92,11 @@ extension SVG { return copy } - public func scale(_ factor: CGFloat) -> SVG { + func scale(_ factor: CGFloat) -> SVG { scale(x: factor, y: factor) } - public func scale(x: CGFloat, y: CGFloat) -> SVG { + func scale(x: CGFloat, y: CGFloat) -> SVG { var copy = self copy.commands.insert(.scale(sx: x, sy: y), at: 0) @@ -107,20 +107,20 @@ extension SVG { return copy } - public func translate(tx: CGFloat, ty: CGFloat) -> SVG { + func translate(tx: CGFloat, ty: CGFloat) -> SVG { var copy = self copy.commands.insert(.translate(tx: tx, ty: ty), at: 0) return copy } - public func expand(_ padding: CGFloat) -> SVG { + func expand(_ padding: CGFloat) -> SVG { expand(top: padding, left: padding, bottom: padding, right: padding) } - public func expand(top: CGFloat = 0, - left: CGFloat = 0, - bottom: CGFloat = 0, - right: CGFloat = 0) -> SVG { + func expand(top: CGFloat = 0, + left: CGFloat = 0, + bottom: CGFloat = 0, + right: CGFloat = 0) -> SVG { var copy = self copy.commands.insert(.translate(tx: left, ty: top), at: 0) copy.size.width += left + right @@ -129,36 +129,6 @@ extension SVG { } } -extension SVG { - - public mutating func sized(_ s: CGSize) { - self = size(s) - } - - public mutating func scaled(_ factor: CGFloat) { - self = scale(factor) - } - - public mutating func scaled(x: CGFloat, y: CGFloat) { - self = scale(x: x, y: y) - } - - public mutating func translated(tx: CGFloat, ty: CGFloat) { - self = translate(tx: tx, ty: ty) - } - - public mutating func expanded(_ padding: CGFloat) { - self = expand(padding) - } - - public mutating func expanded(top: CGFloat = 0, - left: CGFloat = 0, - bottom: CGFloat = 0, - right: CGFloat = 0) { - self = expand(top: top, left: left, bottom: bottom, right: right) - } -} - extension SVG { init(dom: DOM.SVG, options: Options) { @@ -222,5 +192,50 @@ public extension SVG { static func pdfData(fileURL url: URL, size: CGSize? = nil) throws -> Data { throw DOM.Error.missing("not implemented") } + + func size(_ s: CGSize) -> SVG { self } + + func scale(_ factor: CGFloat) -> SVG { self } + + func scale(x: CGFloat, y: CGFloat) -> SVG { self } + + func translate(tx: CGFloat, ty: CGFloat) -> SVG { self } + + func expand(_ padding: CGFloat) -> SVG { self } + + func expand(top: CGFloat = 0, + left: CGFloat = 0, + bottom: CGFloat = 0, + right: CGFloat = 0) -> SVG { self } } #endif + +public extension SVG { + + mutating func sized(_ s: CGSize) { + self = size(s) + } + + mutating func scaled(_ factor: CGFloat) { + self = scale(factor) + } + + mutating func scaled(x: CGFloat, y: CGFloat) { + self = scale(x: x, y: y) + } + + mutating func translated(tx: CGFloat, ty: CGFloat) { + self = translate(tx: tx, ty: ty) + } + + mutating func expanded(_ padding: CGFloat) { + self = expand(padding) + } + + mutating func expanded(top: CGFloat = 0, + left: CGFloat = 0, + bottom: CGFloat = 0, + right: CGFloat = 0) { + self = expand(top: top, left: left, bottom: bottom, right: right) + } +} From 9df5bfe408f6310a1a99cc2bd16077ec3dd4dfd9 Mon Sep 17 00:00:00 2001 From: Simon Whitty Date: Sun, 23 Feb 2025 21:15:45 +1100 Subject: [PATCH 5/6] Deprecate Insets --- SwiftDraw/NSImage+Image.swift | 20 +- SwiftDraw/SVG+CoreGraphics.swift | 33 +--- SwiftDraw/SVG+Deprecated.swift | 88 +++++++++ SwiftDraw/UIImage+Image.swift | 57 ++---- SwiftDrawTests/Image+CoreGraphicsTests.swift | 185 +++---------------- SwiftDrawTests/SVGTests.swift | 19 +- 6 files changed, 156 insertions(+), 246 deletions(-) create mode 100644 SwiftDraw/SVG+Deprecated.swift diff --git a/SwiftDraw/NSImage+Image.swift b/SwiftDraw/NSImage+Image.swift index 84184e01..65369785 100644 --- a/SwiftDraw/NSImage+Image.swift +++ b/SwiftDraw/NSImage+Image.swift @@ -72,6 +72,7 @@ public extension NSImage { } public extension SVG { + func rasterize() -> NSImage { return rasterize(with: size) } @@ -91,8 +92,9 @@ public extension SVG { return image } - func pngData(size: CGSize? = nil, scale: CGFloat = 0, insets: Insets = .zero) throws -> Data { - let (bounds, pixelsWide, pixelsHigh) = makeBounds(size: size, scale: scale, insets: insets) + func pngData(scale: CGFloat = 0) throws -> Data { + let scale = scale == 0 ? SVG.defaultScale : scale + let (bounds, pixelsWide, pixelsHigh) = Self.makeBounds(size: size, scale: scale) guard let bitmap = makeBitmap(width: pixelsWide, height: pixelsHigh, isOpaque: false), let ctx = NSGraphicsContext(bitmapImageRep: bitmap)?.cgContext else { throw Error("Failed to create CGContext") @@ -108,8 +110,9 @@ public extension SVG { return data } - func jpegData(size: CGSize? = nil, scale: CGFloat = 0, compressionQuality quality: CGFloat = 1, insets: Insets = .zero) throws -> Data { - let (bounds, pixelsWide, pixelsHigh) = makeBounds(size: size, scale: scale, insets: insets) + func jpegData(scale: CGFloat = 0, compressionQuality quality: CGFloat = 1) throws -> Data { + let scale = scale == 0 ? SVG.defaultScale : scale + let (bounds, pixelsWide, pixelsHigh) = Self.makeBounds(size: size, scale: scale) guard let bitmap = makeBitmap(width: pixelsWide, height: pixelsHigh, isOpaque: true), let ctx = NSGraphicsContext(bitmapImageRep: bitmap)?.cgContext else { throw Error("Failed to create CGContext") @@ -127,6 +130,10 @@ public extension SVG { return data } + internal static var defaultScale: CGFloat { + NSScreen.main?.backingScaleFactor ?? 1.0 + } + private struct Error: LocalizedError { var errorDescription: String? @@ -138,11 +145,6 @@ public extension SVG { extension SVG { - func makeBounds(size: CGSize?, scale: CGFloat, insets: Insets) -> (bounds: CGRect, pixelsWide: Int, pixelsHigh: Int) { - let scale = scale == 0 ? (NSScreen.main?.backingScaleFactor ?? 1.0) : scale - return Self.makeBounds(size: size, defaultSize: self.size, scale: scale, insets: insets) - } - func makeBitmap(width: Int, height: Int, isOpaque: Bool) -> NSBitmapImageRep? { guard width > 0 && height > 0 else { return nil } return NSBitmapImageRep( diff --git a/SwiftDraw/SVG+CoreGraphics.swift b/SwiftDraw/SVG+CoreGraphics.swift index 785cc5cc..8f6edae4 100644 --- a/SwiftDraw/SVG+CoreGraphics.swift +++ b/SwiftDraw/SVG+CoreGraphics.swift @@ -55,8 +55,8 @@ public extension CGContext { public extension SVG { - func pdfData(size: CGSize? = nil, insets: Insets = .zero) throws -> Data { - let (bounds, pixelsWide, pixelsHigh) = makeBounds(size: size, scale: 1, insets: insets) + func pdfData() throws -> Data { + let (bounds, pixelsWide, pixelsHigh) = Self.makeBounds(size: size, scale: 1) var mediaBox = CGRect(x: 0.0, y: 0.0, width: CGFloat(pixelsWide), height: CGFloat(pixelsHigh)) let data = NSMutableData() @@ -86,31 +86,18 @@ public extension SVG { extension SVG { - static func makeBounds(size: CGSize?, - defaultSize: CGSize, - scale: CGFloat, - insets: Insets) -> (bounds: CGRect, pixelsWide: Int, pixelsHigh: Int) { - let viewport = CGSize( - width: defaultSize.width - (insets.left + insets.right), - height: defaultSize.height - (insets.top + insets.bottom) + static func makeBounds(size: CGSize, scale: CGFloat) -> (bounds: CGRect, pixelsWide: Int, pixelsHigh: Int) { + let bounds = CGRect( + x: 0, + y: 0, + width: size.width * scale, + height: size.height * scale ) - let size = size ?? viewport - - let sx = size.width / viewport.width - let sy = size.height / viewport.height - - let width = size.width * scale - let height = size.height * scale - let insets = insets.applying(sx: sx * scale, sy: sy * scale) - let bounds = CGRect(x: -insets.left, - y: -insets.top, - width: width + insets.left + insets.right, - height: height + insets.top + insets.bottom) return ( bounds: bounds, - pixelsWide: Int(width), - pixelsHigh: Int(height) + pixelsWide: Int(exactly: ceil(bounds.width)) ?? 0, + pixelsHigh: Int(exactly: ceil(bounds.height)) ?? 0 ) } } diff --git a/SwiftDraw/SVG+Deprecated.swift b/SwiftDraw/SVG+Deprecated.swift new file mode 100644 index 00000000..bf5683f9 --- /dev/null +++ b/SwiftDraw/SVG+Deprecated.swift @@ -0,0 +1,88 @@ +// +// SVG+Deprecated.swift +// SwiftDraw +// +// Created by Simon Whitty on 23/2/25. +// Copyright 2025 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. +// + +#if canImport(CoreGraphics) +import CoreGraphics +import Foundation + +#if canImport(UIKit) +import UIKit +#endif + +public extension SVG { + + @available(*, deprecated, message: "add insets via SVG.expand() before pngData") + func pngData(scale: CGFloat = 0, insets: Insets) throws -> Data { + try inset(insets).pngData(scale: scale) + } + + @available(*, deprecated, message: "set size via SVG.size() before pngData") + func pngData(size: CGSize, scale: CGFloat = 0) throws -> Data { + try self.size(size).pngData(scale: scale) + } + + @available(*, deprecated, message: "add insets via SVG.expand() before jpegData") + func jpegData(scale: CGFloat = 0, compressionQuality quality: CGFloat = 1, insets: Insets) throws -> Data { + try inset(insets).jpegData(scale: scale, compressionQuality: quality) + } + + @available(*, deprecated, message: "set size via SVG.size() before jpegData") + func jpegData(size: CGSize, scale: CGFloat = 0, compressionQuality quality: CGFloat = 1) throws -> Data { + try self.size(size).jpegData(scale: scale, compressionQuality: quality) + } + + private func inset(_ insets: Insets) -> SVG { + expand(top: -insets.top, left: -insets.left, bottom: -insets.bottom, right: -insets.right) + } + +#if canImport(UIKit) + @available(*, deprecated, message: "add insets via SVG.expand() before rasterize()") + func rasterize(scale: CGFloat = 0, insets: UIEdgeInsets) -> UIImage { + inset(insets).rasterize(scale: scale) + } + + @available(*, deprecated, message: "add insets via SVG.expand() before pngData()") + func pngData(scale: CGFloat = 0, insets: UIEdgeInsets) throws -> Data { + try inset(insets).pngData(scale: scale) + } + + @available(*, deprecated, message: "add insets via SVG.expand() before jpegData()") + func jpegData(scale: CGFloat = 0, compressionQuality quality: CGFloat = 1, insets: UIEdgeInsets) throws -> Data { + try inset(insets).jpegData(scale: scale, compressionQuality: quality) + } + + private func inset(_ insets: UIEdgeInsets) -> SVG { + expand(top: -insets.top, left: -insets.left, bottom: -insets.bottom, right: -insets.right) + } +#endif + +} +#endif diff --git a/SwiftDraw/UIImage+Image.swift b/SwiftDraw/UIImage+Image.swift index 0f8e1aa1..60c8ea53 100644 --- a/SwiftDraw/UIImage+Image.swift +++ b/SwiftDraw/UIImage+Image.swift @@ -71,16 +71,11 @@ public extension UIImage { } public extension SVG { - func rasterize() -> UIImage { - return rasterize(with: size) - } #if os(watchOS) - func rasterize(with size: CGSize? = nil, scale: CGFloat = 0, insets: UIEdgeInsets = .zero) -> UIImage { - let insets = Insets(top: insets.top, left: insets.left, bottom: insets.bottom, right: insets.right) - let (bounds, pixelsWide, pixelsHigh) = makeBounds(size: size, scale: 1, insets: insets) - - let actualScale = scale <= 0 ? WKInterfaceDevice.current().screenScale : scale + func rasterize(scale: CGFloat = 0) -> UIImage { + let (bounds, pixelsWide, pixelsHigh) = SVG.makeBounds(size: size, scale: 1) + let actualScale = scale <= 0 ? SVG.defaultScale : scale UIGraphicsBeginImageContextWithOptions(CGSize(width: pixelsWide, height: pixelsHigh), false, actualScale) defer { UIGraphicsEndImageContext() } @@ -91,40 +86,29 @@ public extension SVG { return UIGraphicsGetImageFromCurrentImageContext() ?? UIImage() } #else - private func makeFormat() -> UIGraphicsImageRendererFormat { - guard #available(iOS 12.0, *) else { - let f = UIGraphicsImageRendererFormat.default() - f.prefersExtendedRange = true - return f - } + func rasterize(scale: CGFloat = 0) -> UIImage { + let (bounds, pixelsWide, pixelsHigh) = SVG.makeBounds(size: size, scale: 1) let f = UIGraphicsImageRendererFormat.preferred() f.preferredRange = .automatic - return f - } - - func rasterize(with size: CGSize? = nil, scale: CGFloat = 0, insets: UIEdgeInsets = .zero) -> UIImage { - let insets = Insets(top: insets.top, left: insets.left, bottom: insets.bottom, right: insets.right) - let (bounds, pixelsWide, pixelsHigh) = makeBounds(size: size, scale: 1, insets: insets) - let f = makeFormat() - f.scale = scale <= 0 ? UIScreen.main.scale : scale + f.scale = scale <= 0 ? SVG.defaultScale : scale f.opaque = false let r = UIGraphicsImageRenderer(size: CGSize(width: pixelsWide, height: pixelsHigh), format: f) - return r.image{ + return r.image { $0.cgContext.draw(self, in: bounds) } } #endif - func pngData(size: CGSize? = nil, scale: CGFloat = 0, insets: UIEdgeInsets = .zero) throws -> Data { - let image = rasterize(with: size, scale: scale, insets: insets) + func pngData(scale: CGFloat = 0) throws -> Data { + let image = rasterize(scale: scale) guard let data = image.pngData() else { throw Error("Failed to create png data") } return data } - func jpegData(size: CGSize? = nil, scale: CGFloat = 0, compressionQuality quality: CGFloat = 1, insets: UIEdgeInsets = .zero) throws -> Data { - let image = rasterize(with: size, scale: scale, insets: insets) + func jpegData(scale: CGFloat = 0, compressionQuality quality: CGFloat = 1) throws -> Data { + let image = rasterize(scale: scale) guard let data = image.jpegData(compressionQuality: quality) else { throw Error("Failed to create jpeg data") } @@ -134,25 +118,12 @@ public extension SVG { extension SVG { - func jpegData(size: CGSize?, scale: CGFloat, insets: Insets) throws -> Data { - let insets = UIEdgeInsets(top: insets.top, left: insets.left, bottom: insets.bottom, right: insets.right) - return try jpegData(size: size, scale: scale, insets: insets) - } - - func pngData(size: CGSize?, scale: CGFloat, insets: Insets) throws -> Data { - let insets = UIEdgeInsets(top: insets.top, left: insets.left, bottom: insets.bottom, right: insets.right) - return try pngData(size: size, scale: scale, insets: insets) - } - - func makeBounds(size: CGSize?, scale: CGFloat, insets: Insets) -> (bounds: CGRect, pixelsWide: Int, pixelsHigh: Int) { - let newScale: CGFloat = { + static var defaultScale: CGFloat { #if os(watchOS) - return scale <= 0 ? WKInterfaceDevice.current().screenScale : scale + WKInterfaceDevice.current().screenScale #else - return scale <= 0 ? UIScreen.main.scale : scale + UIScreen.main.scale #endif - }() - return Self.makeBounds(size: size, defaultSize: self.size, scale: newScale, insets: insets) } private struct Error: LocalizedError { diff --git a/SwiftDrawTests/Image+CoreGraphicsTests.swift b/SwiftDrawTests/Image+CoreGraphicsTests.swift index 0292d0b0..8027946a 100644 --- a/SwiftDrawTests/Image+CoreGraphicsTests.swift +++ b/SwiftDrawTests/Image+CoreGraphicsTests.swift @@ -36,200 +36,61 @@ import CoreGraphics final class ImageCoreGraphicsTests: XCTestCase { - func testPixelWide_WithInsetsZero() { + func testPixelWide() { XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .zero).pixelsWide, + SVG.makeBounds(size: CGSize(width: 100, height: 50), + scale: 1).pixelsWide, 100 ) XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .zero).pixelsWide, + SVG.makeBounds(size: CGSize(width: 100.5, height: 50), + scale: 1).pixelsWide, + 101 + ) + XCTAssertEqual( + SVG.makeBounds(size: CGSize(width: 100, height: 50), + scale: 2).pixelsWide, 200 ) XCTAssertEqual( SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .zero).pixelsWide, + scale: 1).pixelsWide, 300 ) XCTAssertEqual( SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .zero).pixelsWide, + scale: 2).pixelsWide, 600 ) } - func testPixelHigh_WithInsetsZero() { + func testPixelHigh() { XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .zero).pixelsHigh, + SVG.makeBounds(size: CGSize(width: 100, height: 50), + scale: 1).pixelsHigh, 50 ) XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .zero).pixelsHigh, - 100 - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .zero).pixelsHigh, - 200 - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .zero).pixelsHigh, - 400 - ) - } - - func testPixelWide_WithInsets() { - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .make(left: 5, right: 20)).pixelsWide, - 75 - ) - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .make(left: 5, right: 20)).pixelsWide, - 150 - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .make(left: 5, right: 20)).pixelsWide, - 300 - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .make(left: 5, right: 20)).pixelsWide, - 600 + SVG.makeBounds(size: CGSize(width: 100, height: 50.5), + scale: 1).pixelsHigh, + 51 ) - } - - func testPixelHigh_WithInsets() { XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .make(top: 15, bottom: 30)).pixelsHigh, - 5 - ) - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .make(top: 15, bottom: 30)).pixelsHigh, - 10 + SVG.makeBounds(size: CGSize(width: 100, height: 50), + scale: 2).pixelsHigh, + 100 ) XCTAssertEqual( SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .make(top: 15, bottom: 30)).pixelsHigh, + scale: 1).pixelsHigh, 200 ) XCTAssertEqual( SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .make(top: 15, bottom: 30)).pixelsHigh, + scale: 2).pixelsHigh, 400 ) } - - func testBounds_WithInsetsZero() { - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .zero).bounds, - CGRect(x: 0, y: 0, width: 100, height: 50) - ) - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .zero).bounds, - CGRect(x: 0, y: 0, width: 200, height: 100) - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .zero).bounds, - CGRect(x: 0, y: 0, width: 300, height: 200) - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .zero).bounds, - CGRect(x: 0, y: 0, width: 600, height: 400) - ) - } - - func testBounds_WithInsets() { - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .make(top: 5, left: 10, bottom: 15, right: 20)).bounds, - CGRect(x: -10, y: -5, width: 100, height: 50) - ) - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .make(top: 5, left: 10, bottom: 15, right: 20)).bounds, - CGRect(x: -20, y: -10, width: 200, height: 100) - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 200, height: 200), - defaultSize: CGSize(width: 100, height: 100), - scale: 1, - insets: .make(top: 10, left: 10, bottom: 10, right: 10)).bounds, - CGRect(x: -25, y: -25, width: 250, height: 250) - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 200, height: 200), - defaultSize: CGSize(width: 100, height: 100), - scale: 2, - insets: .make(top: 10, left: 10, bottom: 10, right: 10)).bounds, - CGRect(x: -50, y: -50, width: 500, height: 500) - ) - } -} - -private extension SVG.Insets { - static func make(top: CGFloat = 0, - left: CGFloat = 0, - bottom: CGFloat = 0, - right: CGFloat = 0) -> Self { - Self(top: top, left: left, bottom: bottom, right: right) - } } #endif diff --git a/SwiftDrawTests/SVGTests.swift b/SwiftDrawTests/SVGTests.swift index 6349808d..ef5849c0 100644 --- a/SwiftDrawTests/SVGTests.swift +++ b/SwiftDrawTests/SVGTests.swift @@ -56,14 +56,14 @@ final class SVGTests: XCTestCase { XCTAssertNoThrow(try image.pdfData()) } - func testImageRasterizeAndScales() { - let image = SVG.makeLines() - let doubleSize = CGSize(width: 200, height: 200) - let rendered = image.rasterize(with: doubleSize, scale: 1) - XCTAssertEqual(rendered.size, doubleSize) - XCTAssertNoThrow(try image.pngData(size: doubleSize)) - XCTAssertNoThrow(try image.jpegData(size: doubleSize)) - } +// func testImageRasterizeAndScales() { +// let image = SVG.makeLines() +// let doubleSize = CGSize(width: 200, height: 200) +// let rendered = image.rasterize(with: doubleSize, scale: 1) +// XCTAssertEqual(rendered.size, doubleSize) +// XCTAssertNoThrow(try image.pngData(size: doubleSize)) +// XCTAssertNoThrow(try image.jpegData(size: doubleSize)) +// } func testShapesImageRasterizes() throws { let image = try XCTUnwrap(SVG(named: "shapes.svg", in: .test)) @@ -75,7 +75,8 @@ final class SVGTests: XCTestCase { #if canImport(UIKit) func testRasterize() { let svg = SVG(named: "gradient-apple.svg", in: .test)! - let image = svg.rasterize(with: CGSize(width: 100, height: 100), scale: 3) + .size(CGSize(width: 100, height: 100)) + let image = svg.rasterize(scale: 3) XCTAssertEqual(image.size, CGSize(width: 100, height: 100)) XCTAssertEqual(image.scale, 3) From dbeb76f34e8994882a76cf8507f1412a4f36fe95 Mon Sep 17 00:00:00 2001 From: Simon Whitty Date: Sun, 23 Feb 2025 21:17:22 +1100 Subject: [PATCH 6/6] renames +SVG --- SwiftDraw/{NSImage+Image.swift => NSImage+SVG.swift} | 2 +- SwiftDraw/{UIImage+Image.swift => UIImage+SVG.swift} | 2 +- .../{NSImage+ImageTests.swift => NSImage+SVGTests.swift} | 4 ++-- .../{UIImage+ImageTests.swift => UIImage+SVGTests.swift} | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) rename SwiftDraw/{NSImage+Image.swift => NSImage+SVG.swift} (99%) rename SwiftDraw/{UIImage+Image.swift => UIImage+SVG.swift} (99%) rename SwiftDrawTests/{NSImage+ImageTests.swift => NSImage+SVGTests.swift} (97%) rename SwiftDrawTests/{UIImage+ImageTests.swift => UIImage+SVGTests.swift} (97%) diff --git a/SwiftDraw/NSImage+Image.swift b/SwiftDraw/NSImage+SVG.swift similarity index 99% rename from SwiftDraw/NSImage+Image.swift rename to SwiftDraw/NSImage+SVG.swift index 65369785..1dcc93fd 100644 --- a/SwiftDraw/NSImage+Image.swift +++ b/SwiftDraw/NSImage+SVG.swift @@ -1,5 +1,5 @@ // -// NSImage+Image.swift +// NSImage+SVG.swift // SwiftDraw // // Created by Simon Whitty on 24/5/17. diff --git a/SwiftDraw/UIImage+Image.swift b/SwiftDraw/UIImage+SVG.swift similarity index 99% rename from SwiftDraw/UIImage+Image.swift rename to SwiftDraw/UIImage+SVG.swift index 60c8ea53..b209629b 100644 --- a/SwiftDraw/UIImage+Image.swift +++ b/SwiftDraw/UIImage+SVG.swift @@ -1,5 +1,5 @@ // -// UIImage+Image.swift +// UIImage+SVG.swift // SwiftDraw // // Created by Simon Whitty on 24/5/17. diff --git a/SwiftDrawTests/NSImage+ImageTests.swift b/SwiftDrawTests/NSImage+SVGTests.swift similarity index 97% rename from SwiftDrawTests/NSImage+ImageTests.swift rename to SwiftDrawTests/NSImage+SVGTests.swift index f2804dc5..b869c556 100644 --- a/SwiftDrawTests/NSImage+ImageTests.swift +++ b/SwiftDrawTests/NSImage+SVGTests.swift @@ -1,5 +1,5 @@ // -// NSImage+ImageTests.swift +// NSImage+SVGTests.swift // SwiftDraw // // Created by Simon Whitty on 27/11/18. @@ -33,7 +33,7 @@ import XCTest @testable import SwiftDraw #if canImport(AppKit) && !targetEnvironment(macCatalyst) -final class NSImageTests: XCTestCase { +final class NSImageSVGTests: XCTestCase { func testImageLoads() { let image = NSImage(svgNamed: "lines.svg", in: .test) diff --git a/SwiftDrawTests/UIImage+ImageTests.swift b/SwiftDrawTests/UIImage+SVGTests.swift similarity index 97% rename from SwiftDrawTests/UIImage+ImageTests.swift rename to SwiftDrawTests/UIImage+SVGTests.swift index 269cb9c7..9aae0c21 100644 --- a/SwiftDrawTests/UIImage+ImageTests.swift +++ b/SwiftDrawTests/UIImage+SVGTests.swift @@ -1,5 +1,5 @@ // -// UIImage+ImageTests.swift +// UIImage+SVGTests.swift // SwiftDraw // // Created by Simon Whitty on 27/11/18. @@ -35,7 +35,7 @@ import XCTest #if canImport(UIKit) import UIKit -final class UIImageTests: XCTestCase { +final class UIImageSVGTests: XCTestCase { func testImageLoads() { let image = UIImage(svgNamed: "lines.svg", in: .test)