-
Notifications
You must be signed in to change notification settings - Fork 589
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: add plugin api surface to allow changing image serialization
- Loading branch information
Showing
24 changed files
with
759 additions
and
176 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
90 changes: 90 additions & 0 deletions
90
Sources/ImageSerializationPlugin/ImageSerializationPlugin.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import Foundation | ||
import SnapshotTestingPlugin | ||
|
||
#if canImport(UIKit) | ||
import UIKit.UIImage | ||
/// A type alias for `UIImage` when UIKit is available. | ||
public typealias SnapImage = UIImage | ||
#elseif canImport(AppKit) | ||
import AppKit.NSImage | ||
/// A type alias for `NSImage` when AppKit is available. | ||
public typealias SnapImage = NSImage | ||
#else | ||
public typealias SnapImage = Data | ||
#endif | ||
|
||
/// A type alias that combines `ImageSerialization` and `SnapshotTestingPlugin` protocols. | ||
/// | ||
/// `ImageSerializationPlugin` is a convenient alias used to conform to both `ImageSerialization` and `SnapshotTestingPlugin` protocols. | ||
/// This allows for image serialization plugins that also support snapshot testing, leveraging the Objective-C runtime while maintaining image serialization capabilities. | ||
public typealias ImageSerializationPlugin = ImageSerialization & SnapshotTestingPlugin | ||
|
||
// TODO: async throws will be added later to encodeImage and decodeImage | ||
/// A protocol that defines methods for encoding and decoding images in various formats. | ||
/// | ||
/// The `ImageSerialization` protocol is intended for classes that provide functionality to serialize (encode) and deserialize (decode) images. | ||
/// Implementing this protocol allows a class to specify the image format it supports and to handle image data conversions. | ||
/// This protocol is designed to be used in environments where SwiftUI is available and supports platform-specific image types via `SnapImage`. | ||
public protocol ImageSerialization { | ||
|
||
/// The image format that the serialization plugin supports. | ||
/// | ||
/// Each conforming class must specify the format it handles, using the `ImageSerializationFormat` enum. This property helps the `ImageSerializer` | ||
/// determine which plugin to use for a given format during image encoding and decoding. | ||
static var imageFormat: ImageSerializationFormat { get } | ||
|
||
/// Encodes a `SnapImage` into a data representation. | ||
/// | ||
/// This method converts the provided image into the appropriate data format. It may eventually support asynchronous operations and error handling using `async throws`. | ||
/// | ||
/// - Parameter image: The image to be encoded. | ||
/// - Returns: The encoded image data, or `nil` if encoding fails. | ||
func encodeImage(_ image: SnapImage) -> Data? | ||
|
||
/// Decodes image data into a `SnapImage`. | ||
/// | ||
/// This method converts the provided data back into an image. It may eventually support asynchronous operations and error handling using `async throws`. | ||
/// | ||
/// - Parameter data: The image data to be decoded. | ||
/// - Returns: The decoded image, or `nil` if decoding fails. | ||
func decodeImage(_ data: Data) -> SnapImage? | ||
} | ||
|
||
/// An enumeration that defines the image formats supported by the `ImageSerialization` protocol. | ||
/// | ||
/// The `ImageSerializationFormat` enum is used to represent various image formats. It includes a predefined case for PNG images and a flexible case for plugins, | ||
/// allowing for the extension of formats via plugins identified by unique string values. | ||
public enum ImageSerializationFormat: RawRepresentable, Sendable, Equatable { | ||
|
||
public static let defaultValue: ImageSerializationFormat = .png | ||
|
||
/// Represents the default image format aka PNG. | ||
case png | ||
|
||
/// Represents a custom image format provided by a plugin. | ||
/// | ||
/// This case allows for the extension of image formats beyond the predefined ones by using a unique string identifier. | ||
case plugins(String) | ||
|
||
/// Initializes an `ImageSerializationFormat` instance from a raw string value. | ||
/// | ||
/// This initializer converts a string value into an appropriate `ImageSerializationFormat` case. | ||
/// | ||
/// - Parameter rawValue: The string representation of the image format. | ||
public init?(rawValue: String) { | ||
switch rawValue { | ||
case "png": self = .png | ||
default: self = .plugins(rawValue) | ||
} | ||
} | ||
|
||
/// The raw string value of the `ImageSerializationFormat`. | ||
/// | ||
/// This computed property returns the string representation of the current image format. | ||
public var rawValue: String { | ||
switch self { | ||
case .png: return "png" | ||
case let .plugins(value): return value | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
92 changes: 92 additions & 0 deletions
92
Sources/SnapshotTesting/Documentation.docc/Articles/ImageSerializationPlugin.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
# Image Serialization Plugin | ||
|
||
Image Serialization Plugin is a plugin based on the PluginAPI, it provides support for encoding and decoding images. It leverages the plugin architecture to extend its support for different image formats without needing to modify the core system. | ||
|
||
Plugins that conform to the `ImageSerializationPlugin` protocol can be registered into the `PluginRegistry` and used to encode or decode images in different formats, such as PNG, JPEG, WebP, HEIC, and more. | ||
|
||
When a plugin supporting a specific image format is available, the `ImageSerializer` can dynamically choose the correct plugin based on the image format required, ensuring modularity and scalability in image handling. | ||
|
||
|
||
# Image Serialization Plugin | ||
|
||
The **Image Serialization Plugin** extends the functionality of the SnapshotTesting library by enabling support for multiple image formats through a plugin architecture. This PluginAPI allows image encoding and decoding to be easily extended without modifying the core logic of the system. | ||
|
||
## Overview | ||
|
||
The **Image Serialization Plugin** provides an interface for encoding and decoding images in various formats. By conforming to both the `ImageSerialization` and `SnapshotTestingPlugin` protocols, it integrates with the broader plugin system, allowing for the seamless addition of new image formats. The default implementation supports PNG, but this architecture allows users to define custom plugins for other formats. | ||
|
||
### Image Serialization Plugin Architecture | ||
|
||
The **Image Serialization Plugin** relies on the PluginAPI that is a combination of protocols and a centralized registry to manage and discover plugins. The architecture allows for dynamic registration of image serialization plugins, which can be automatically discovered at runtime using the Objective-C runtime. This makes the system highly extensible, with plugins being automatically registered without the need for manual intervention. | ||
|
||
#### Key Components: | ||
|
||
1. **`ImageSerialization` Protocol**: | ||
- Defines the core methods for encoding and decoding images. | ||
- Requires plugins to specify the image format they support using the `ImageSerializationFormat` enum. | ||
- Provides methods for encoding (`encodeImage`) and decoding (`decodeImage`) images. | ||
|
||
2. **`ImageSerializationFormat` Enum**: | ||
- Represents supported image formats. | ||
- Includes predefined formats such as `.png` and extensible formats through the `.plugins(String)` case, allowing for custom formats to be introduced via plugins. | ||
|
||
3. **`ImageSerializer` Class**: | ||
- Responsible for encoding and decoding images using the registered plugins. | ||
- Retrieves available plugins from the `PluginRegistry` and uses the first matching plugin for the requested image format. | ||
- Provides default implementations for PNG encoding and decoding if no plugin is available for a given format. | ||
|
||
#### Example Plugin Flow: | ||
|
||
1. **Plugin Discovery**: | ||
- Plugins are automatically discovered at runtime through the Objective-C runtime, which identifies classes that conform to both the `ImageSerialization` and `SnapshotTestingPlugin` protocols. | ||
|
||
2. **Plugin Registration**: | ||
- Each plugin registers itself with the `PluginRegistry`, allowing it to be retrieved when needed for image serialization. | ||
|
||
3. **Image Encoding/Decoding**: | ||
- When an image needs to be serialized, the `ImageSerializer` checks the available plugins for one that supports the requested format. | ||
- If no plugin is found, it defaults to the built-in PNG encoding/decoding methods. | ||
|
||
#### Extensibility | ||
|
||
The plugin architecture allows developers to introduce new image formats without modifying the core SnapshotTesting library. By creating a new plugin that conforms to `ImageSerializationPlugin`, you can easily add support for additional image formats. | ||
|
||
Here are a few example plugins demonstrating how to extend the library with new image formats: | ||
|
||
- **[Image Serialization Plugin - HEIC](https://github.com/mackoj/swift-snapshot-testing-plugin-heic)**: Enables storing images in the `.heic` format, which reduces file sizes compared to PNG. | ||
- **[Image Serialization Plugin - WEBP](https://github.com/mackoj/swift-snapshot-testing-plugin-webp)**: Allows storing images in the `.webp` format, which offers better compression than PNG. | ||
- **[Image Serialization Plugin - JXL](https://github.com/mackoj/swift-snapshot-testing-plugin-jxl)**: Facilitates storing images in the `.jxl` format, which provides superior compression and quality compared to PNG. | ||
|
||
## Usage | ||
|
||
For example, if you want to use JPEG XL as a new image format for your snapshots, you can follow these steps. This approach applies to any image format as long as you have a plugin that conforms to `ImageSerializationPlugin`. | ||
|
||
1. **Add the Dependency**: Include the appropriate image serialization plugin as a dependency in your `Package.swift` file. For JPEG XL, it would look like this: | ||
|
||
```swift | ||
.package(url: "https://github.com/mackoj/swift-snapshot-testing-plugin-jxl.git", revision: "0.0.1"), | ||
``` | ||
|
||
2. **Link to Your Test Target**: Add the image serialization plugin to your test target's dependencies: | ||
|
||
```swift | ||
.product(name: "JXLImageSerializer", package: "swift-snapshot-testing-plugin-jxl"), | ||
``` | ||
|
||
3. **Import and Set Up**: In your test file, import the serializer and configure the image format in the `setUp()` method: | ||
|
||
```swift | ||
import JXLImageSerializer | ||
|
||
override class func setUp() { | ||
SnapshotTesting.imageFormat = JXLImageSerializer.imageFormat | ||
} | ||
``` | ||
|
||
Alternatively, you can specify the image format for individual assertions: | ||
|
||
```swift | ||
assertSnapshot(of: label, as: .image(precision: 0.9, format: JXLImageSerializer.imageFormat)) | ||
``` | ||
|
||
This setup demonstrates how to integrate a specific image format plugin. Replace `JXLImageSerializer` with the appropriate plugin and format for other image formats. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import Foundation | ||
import ImageSerializationPlugin | ||
|
||
#if canImport(UIKit) | ||
import UIKit | ||
#elseif canImport(AppKit) | ||
import AppKit | ||
#endif | ||
|
||
/// A class responsible for encoding and decoding images using various image serialization plugins. | ||
/// | ||
/// The `ImageSerializer` class leverages plugins that conform to the `ImageSerialization` protocol to encode and decode images in different formats. | ||
/// It automatically retrieves all available image serialization plugins from the `PluginRegistry` and uses them based on the specified `ImageSerializationFormat`. | ||
/// If no plugin is found for the requested format, it defaults to using PNG encoding/decoding. | ||
class ImageSerializer { | ||
|
||
/// A collection of plugins that conform to the `ImageSerialization` protocol. | ||
private let plugins: [ImageSerialization] | ||
|
||
init() { | ||
self.plugins = PluginRegistry.allPlugins() | ||
} | ||
|
||
// TODO: async throws will be added later | ||
/// Encodes a given image into the specified image format using the appropriate plugin. | ||
/// | ||
/// This method attempts to encode the provided `SnapImage` into the desired format using the first plugin that supports the specified `ImageSerializationFormat`. | ||
/// If no plugin is found for the format, it defaults to encoding the image as PNG. | ||
/// | ||
/// - Parameters: | ||
/// - image: The `SnapImage` to encode. | ||
/// - imageFormat: The format in which to encode the image. | ||
/// - Returns: The encoded image data, or `nil` if encoding fails. | ||
func encodeImage(_ image: SnapImage, imageFormat: ImageSerializationFormat = .defaultValue) -> Data? { | ||
for plugin in self.plugins { | ||
if type(of: plugin).imageFormat == imageFormat { | ||
return plugin.encodeImage(image) | ||
} | ||
} | ||
// Default to PNG | ||
return encodePNG(image) | ||
} | ||
|
||
// TODO: async throws will be added later | ||
/// Decodes image data into a `SnapImage` using the appropriate plugin based on the specified image format. | ||
/// | ||
/// This method attempts to decode the provided data into a `SnapImage` using the first plugin that supports the specified `ImageSerializationFormat`. | ||
/// If no plugin is found for the format, it defaults to decoding the data as PNG. | ||
/// | ||
/// - Parameters: | ||
/// - data: The image data to decode. | ||
/// - imageFormat: The format in which the image data is encoded. | ||
/// - Returns: The decoded `SnapImage`, or `nil` if decoding fails. | ||
func decodeImage(_ data: Data, imageFormat: ImageSerializationFormat = .defaultValue) -> SnapImage? { | ||
for plugin in self.plugins { | ||
if type(of: plugin).imageFormat == imageFormat { | ||
return plugin.decodeImage(data) | ||
} | ||
} | ||
// Default to PNG | ||
return decodePNG(data) | ||
} | ||
|
||
// MARK: - Actual default Image Serializer | ||
|
||
/// Encodes a `SnapImage` as PNG data. | ||
/// | ||
/// This method provides a default implementation for encoding images as PNG. It is used as a fallback if no suitable plugin is found for the requested format. | ||
/// | ||
/// - Parameter image: The `SnapImage` to encode. | ||
/// - Returns: The encoded PNG data, or `nil` if encoding fails. | ||
private func encodePNG(_ image: SnapImage) -> Data? { | ||
#if canImport(UIKit) | ||
return image.pngData() | ||
#elseif canImport(AppKit) | ||
guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { | ||
return nil | ||
} | ||
let bitmapRep = NSBitmapImageRep(cgImage: cgImage) | ||
return bitmapRep.representation(using: .png, properties: [:]) | ||
#endif | ||
} | ||
|
||
/// Decodes PNG data into a `SnapImage`. | ||
/// | ||
/// This method provides a default implementation for decoding PNG data into a `SnapImage`. It is used as a fallback if no suitable plugin is found for the requested format. | ||
/// | ||
/// - Parameter data: The PNG data to decode. | ||
/// - Returns: The decoded `SnapImage`, or `nil` if decoding fails. | ||
private func decodePNG(_ data: Data) -> SnapImage? { | ||
#if canImport(UIKit) | ||
return UIImage(data: data) | ||
#elseif canImport(AppKit) | ||
return NSImage(data: data) | ||
#endif | ||
} | ||
} |
Oops, something went wrong.