diff --git a/Package.swift b/Package.swift index d9806df..1aa55b6 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/SVGKit/SVGKit.git", from: "3.0.0"), - .package(url: "https://github.com/Snapp-Mobile/SnappTheming", from: "0.0.8"), + .package(url: "https://github.com/Snapp-Mobile/SnappTheming", branch: "main"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/Sources/SnappThemingSVGSupport/Extensions/SVGKImage+themeImage.swift b/Sources/SnappThemingSVGSupport/Extensions/SVGKImage+themeImage.swift new file mode 100644 index 0000000..a8d3b4c --- /dev/null +++ b/Sources/SnappThemingSVGSupport/Extensions/SVGKImage+themeImage.swift @@ -0,0 +1,19 @@ +// +// SVGKImage+themeImage.swift +// SnappThemingSVGSupport +// +// Created by Oleksii Kolomiiets on 24.02.2025. +// + +import SVGKit +import SnappTheming + +extension SVGKImage { + var themeImage: SnappThemingImage? { + #if canImport(UIKit) + self.uiImage + #elseif canImport(AppKit) + self.nsImage + #endif + } +} diff --git a/Sources/SnappThemingSVGSupport/SnappThemingSVGSupportImageConverter.swift b/Sources/SnappThemingSVGSupport/SnappThemingSVGSupportImageConverter.swift index 7d28604..fa88306 100644 --- a/Sources/SnappThemingSVGSupport/SnappThemingSVGSupportImageConverter.swift +++ b/Sources/SnappThemingSVGSupport/SnappThemingSVGSupportImageConverter.swift @@ -8,83 +8,51 @@ import Foundation import OSLog import SVGKit - -#if canImport(UIKit) - import UIKit -#elseif canImport(AppKit) - import AppKit -#endif +import SnappTheming /// A utility class for converts SVG data into a `UIImage`. -class SnappThemingSVGSupportImageConverter { - #if canImport(UIKit) - /// The rendered `UIImage` representation of the SVG data. - private(set) var uiImage: UIImage - #elseif canImport(AppKit) - /// The rendered `NSImage` representation of the SVG data. - private(set) var nsImage: NSImage - #endif +final class SnappThemingSVGSupportImageConverter { + /// The rendered `UIImage` representation of the SVG data. + private(set) var image: SnappThemingImage /// Initializes the `SnappThemingSVGSupportImageConverter` with the provided SVG data. /// - /// - Parameter data: The SVG data to be rendered into a `UIImage`. - /// - Note: If the SVG data is invalid or cannot be rendered, a default system image is used as a fallback. - init(data: Data) { - var svgImage: SVGKImage? = SVGKImage(data: data) + /// - Parameters: + /// - data: The SVG data to be rendered into a `SnappThemingImage`. + /// - svgImageType: The type used for SVG rendering, defaulting to `SVGKImage.self`. + /// - fallbackImageName: The system image name used as a fallback if the SVG data is invalid or cannot be rendered. + /// + /// - Note: If the SVG data is invalid or rendering fails, a system image with the name specified in `fallbackImageName` + /// (default: `"exclamationmark.triangle"`) will be used instead. + init( + data: Data, + svgImageType: SVGKImage.Type = SVGKImage.self, + fallbackImageName: String = "exclamationmark.triangle" + ) { + var svgImage: SVGKImage? = svgImageType.init(data: data) // Retry logic for simulator or potential parsing issues // error: `*** Assertion failure in +[SVGLength pixelsPerInchForCurrentDevice], SVGLength.m:238` if svgImage == nil { os_log(.info, "Initial SVG parsing failed. Retrying...") - svgImage = SVGKImage(data: data) + svgImage = svgImageType.init(data: data) } - if let validSVGImage = svgImage { - // Scale the SVG to a maximum size, maintaining its aspect ratio - let targetSize = CGSize( - width: max(validSVGImage.size.width, 512), - height: max(validSVGImage.size.height, 512) - ) - validSVGImage.scaleToFit(inside: targetSize) - #if canImport(UIKit) - // Attempt to render the scaled SVG into a UIImage - if let renderedImage = validSVGImage.uiImage { - self.uiImage = renderedImage - } else { - self.uiImage = Self.defaultFallbackImage - os_log(.error, "Failed to render SVG to UIImage after scaling. Using fallback image.") - } - #elseif canImport(AppKit) - // Attempt to render the scaled SVG into a NSImage - if let renderedImage = validSVGImage.nsImage { - self.nsImage = renderedImage - } else { - self.nsImage = Self.defaultFallbackImage - os_log(.error, "Failed to render SVG to UIImage after scaling. Using fallback image.") - } - #endif + if let validSVGImage = svgImage { + if let renderedImage = validSVGImage.themeImage { + self.image = renderedImage + } else { + self.image = Self.defaultFallbackImage(fallbackImageName) + os_log(.error, "Failed to render SVG to UIImage. Using fallback image.") + } } else { - #if canImport(UIKit) - self.uiImage = Self.defaultFallbackImage - #elseif canImport(AppKit) - self.nsImage = Self.defaultFallbackImage - #endif + self.image = Self.defaultFallbackImage(fallbackImageName) os_log(.error, "Failed to parse SVG data after retry. Using fallback image.") } } - #if canImport(UIKit) - /// The default fallback image to use when SVG rendering fails. - private static var defaultFallbackImage: UIImage { - UIImage(systemName: "exclamationmark.triangle") ?? UIImage() - } - #elseif canImport(AppKit) - /// The default fallback image to use when SVG rendering fails. - private static var defaultFallbackImage: NSImage { - NSImage( - systemSymbolName: "exclamationmark.triangle", - accessibilityDescription: "Missing image" - ) ?? NSImage() - } - #endif + /// The default fallback image to use when SVG rendering fails. + private static func defaultFallbackImage(_ name: String) -> SnappThemingImage { + .system(name) ?? SnappThemingImage() + } } diff --git a/Sources/SnappThemingSVGSupport/SnappThemingSVGSupportSVGProcessor.swift b/Sources/SnappThemingSVGSupport/SnappThemingSVGSupportSVGProcessor.swift index ade7fa7..69617ab 100644 --- a/Sources/SnappThemingSVGSupport/SnappThemingSVGSupportSVGProcessor.swift +++ b/Sources/SnappThemingSVGSupport/SnappThemingSVGSupportSVGProcessor.swift @@ -7,16 +7,9 @@ import Foundation import OSLog -import SVGKit import SnappTheming import UniformTypeIdentifiers -#if canImport(UIKit) - import UIKit -#elseif canImport(AppKit) - import AppKit -#endif - /// Extension for `SnappThemingExternalImageProcessorProtocol` to provide a default implementation for SVG processing. extension SnappThemingExternalImageProcessorProtocol where Self == SnappThemingSVGSupportSVGProcessor { /// Provides a static instance of `SnappThemingSVGSupportSVGProcessor`. @@ -27,45 +20,25 @@ extension SnappThemingExternalImageProcessorProtocol where Self == SnappThemingS /// A processor for handling SVG image data, conforming to `SnappThemingExternalImageProcessorProtocol`. public struct SnappThemingSVGSupportSVGProcessor: SnappThemingExternalImageProcessorProtocol { - #if canImport(UIKit) - /// Processes the provided image data and type and converts it into a `UIImage` if the type is `.svg`. - /// - /// - Parameter data: Image `Data`. - /// - Parameter type: Image `UTType`. - /// - Returns: A `UIImage` if the processing and conversion are successful; otherwise, `nil`. - /// - /// - Note: This method specifically handles `.svg` type. If the type does not match, the method returns `nil`. - /// - Warning: Ensure that the `data` is valid SVG data to avoid potential rendering issues. - public func process(_ data: Data, of type: UTType) -> UIImage? { - guard type == .svg else { - os_log(.error, "Invalid type provided: %{public}@. Only .svg type is supported.", "\(type)") - return nil - } - - let uiImage = SnappThemingSVGSupportImageConverter(data: data).uiImage - - return uiImage + /// Processes the provided image data and type and converts it into a `SnappThemingImage` if the type is `.svg`. + /// + /// - Parameter data: Image `Data`. + /// - Parameter type: Image `UTType`. + /// - Returns: A `SnappThemingImage` if the processing and conversion are successful; otherwise, `nil`. + /// + /// - On **iOS, iPadOS, tvOS, watchOS**, and **visionOS**, `SnappThemingImage` is an alias for `UIImage`. + /// - On **macOS**, `SnappThemingImage` is an alias for `NSImage`. + /// + /// - Note: This method specifically handles `.svg` type. If the type does not match, the method returns `nil`. + /// - Warning: Ensure that the `data` is valid SVG data to avoid potential rendering issues. + public func process(_ data: Data, of type: UTType) -> SnappThemingImage? { + guard type == .svg else { + os_log(.error, "Invalid type provided: %{public}@. Only .svg type is supported.", "\(type)") + return nil } - #elseif canImport(AppKit) - /// Processes the provided image data and type and converts it into a `NSImage` if the type is `.svg`. - /// - /// - Parameter data: Image `Data`. - /// - Parameter type: Image `UTType`. - /// - Returns: A `NSImage` if the processing and conversion are successful; otherwise, `nil`. - /// - /// - Note: This method specifically handles `.svg` type. If the type does not match, the method returns `nil`. - /// - Warning: Ensure that the `data` is valid SVG data to avoid potential rendering issues. - public func process(_ data: Data, of type: UTType) -> NSImage? { - guard type == .svg else { - os_log(.error, "Invalid type provided: %{public}@. Only .svg type is supported.", "\(type)") - return nil - } - let nsImage = SnappThemingSVGSupportImageConverter(data: data).nsImage - - return nsImage - } - #endif + return SnappThemingSVGSupportImageConverter(data: data).image + } /// Initializes the `SnappThemingSVGSupportSVGProcessor`. /// diff --git a/Tests/SnappThemingSVGSupportTests/Fakes/SVGIconString.swift b/Tests/SnappThemingSVGSupportTests/Fakes/SVGIconString.swift new file mode 100644 index 0000000..753c97d --- /dev/null +++ b/Tests/SnappThemingSVGSupportTests/Fakes/SVGIconString.swift @@ -0,0 +1,16 @@ +// +// File.swift +// SnappThemingSVGSupport +// +// Created by Oleksii Kolomiiets on 24.02.2025. +// + +import Foundation + +let svgIconString: String = + """ + + + + + """ diff --git a/Tests/SnappThemingSVGSupportTests/ImageConverterTests.swift b/Tests/SnappThemingSVGSupportTests/ImageConverterTests.swift new file mode 100644 index 0000000..0f59bef --- /dev/null +++ b/Tests/SnappThemingSVGSupportTests/ImageConverterTests.swift @@ -0,0 +1,60 @@ +// +// Test.swift +// SnappThemingSVGSupport +// +// Created by Oleksii Kolomiiets on 24.02.2025. +// + +import Foundation +import SnappTheming +import Testing + +@testable import SnappThemingSVGSupport + +@Suite +struct ImageConverterTests { + @Test + func testConverterSuccessfulConversionExpectedImage() throws { + let expectedImageData = try #require(svgIconString.data(using: .utf8)) + let converter = SnappThemingSVGSupportImageConverter(data: expectedImageData) + + #expect(converter.image.size == CGSize(width: 24, height: 24)) + } + + @Test + func testConverterSVGImageFailedFallbackImage() throws { + let expectedImageData = try #require(svgIconString.data(using: .utf8)) + let converter = SnappThemingSVGSupportImageConverter(data: expectedImageData, svgImageType: MockSVGKImage.self) + let fallbackImage: SnappThemingImage = try #require(.system("exclamationmark.triangle")) + + #expect(converter.image.size != CGSize(width: 24, height: 24)) + #if canImport(UIKit) + #expect(converter.image == fallbackImage) + #elseif canImport(AppKit) + #expect(converter.image.tiffRepresentation == fallbackImage.tiffRepresentation) + #endif + } + + @Test + func testConverterEmptyDataFallbackImage() async throws { + let converter = SnappThemingSVGSupportImageConverter(data: Data()) + let fallbackImage: SnappThemingImage = try #require(.system("exclamationmark.triangle")) + #if canImport(UIKit) + #expect(converter.image == fallbackImage) + #elseif canImport(AppKit) + #expect(converter.image.tiffRepresentation == fallbackImage.tiffRepresentation) + #endif + } + + @Test + func testConverterEmptyDataAndBrokenFallbackImage() async throws { + let converter = SnappThemingSVGSupportImageConverter(data: Data(), fallbackImageName: "") + let fallbackImage: SnappThemingImage = try #require(.system("exclamationmark.triangle")) + #if canImport(UIKit) + #expect(converter.image == SnappThemingImage()) + #elseif canImport(AppKit) + #expect(converter.image.tiffRepresentation != fallbackImage.tiffRepresentation) + #expect(converter.image.tiffRepresentation == nil) + #endif + } +} diff --git a/Tests/SnappThemingSVGSupportTests/Mocks/MockSVGKImage.swift b/Tests/SnappThemingSVGSupportTests/Mocks/MockSVGKImage.swift new file mode 100644 index 0000000..e05882e --- /dev/null +++ b/Tests/SnappThemingSVGSupportTests/Mocks/MockSVGKImage.swift @@ -0,0 +1,20 @@ +// +// MockSVGKImage.swift +// SnappThemingSVGSupport +// +// Created by Oleksii Kolomiiets on 24.02.2025. +// + +import SVGKit + +final class MockSVGKImage: SVGKImage { + #if canImport(UIKit) + override var uiImage: UIImage! { + return nil + } + #elseif canImport(AppKit) + override var nsImage: NSImage! { + return nil + } + #endif +} diff --git a/Tests/SnappThemingSVGSupportTests/SVGProcessorTests.swift b/Tests/SnappThemingSVGSupportTests/SVGProcessorTests.swift new file mode 100644 index 0000000..7d5be5b --- /dev/null +++ b/Tests/SnappThemingSVGSupportTests/SVGProcessorTests.swift @@ -0,0 +1,52 @@ +// +// SnappThemingSVGSupportSVGProcessorTests.swift +// SnappThemingSVGSupport +// +// Created by Oleksii Kolomiiets on 24.02.2025. +// + +import Foundation +import SnappTheming +import Testing + +@testable import SnappThemingSVGSupport + +@Suite +struct SVGProcessorTests { + let sut: SnappThemingSVGSupportSVGProcessor = .svg + + @Test + func testSVGProcessorFail() { + #expect(sut.process(Data(), of: .png) == nil) + #expect(sut.process(Data(), of: .jpeg) == nil) + #expect(sut.process(Data(), of: .pdf) == nil) + #expect(sut.process(Data(), of: .gif) == nil) + } + + @Test + func testSVGProcessorFallback() throws { + let emptyStringData = try #require("".data(using: .utf8)) + let fallbackImage: SnappThemingImage = try #require(.system("exclamationmark.triangle")) + + let emptyStringDataImage = try #require(sut.process(emptyStringData, of: .svg)) + let emptyDataImage = try #require(sut.process(Data(), of: .svg)) + + #if canImport(UIKit) + #expect(emptyStringDataImage == fallbackImage) + #expect(emptyDataImage == fallbackImage) + #elseif canImport(AppKit) + #expect(emptyStringDataImage.tiffRepresentation == fallbackImage.tiffRepresentation) + #expect(emptyDataImage.tiffRepresentation == fallbackImage.tiffRepresentation) + #endif + + } + + @Test + func testSVGProcessorExpectedImage() throws { + let svgData = try #require(svgIconString.data(using: .utf8)) + let processedIcon = try #require(sut.process(svgData, of: .svg)) + + #expect(processedIcon.size.width == 24) + #expect(processedIcon.size.height == 24) + } +}