📷 A composable image editor using Core Image and Metal.
Swift
3,679
666 commits
updated Sep 27, 2026
A composable image editor powered by Core Image and Metal.
Build a photo editor with the included Photos-style UI, compose your own editing interface, or use the rendering engine on its own. Brightroom keeps edits as parameters, so you can change adjustments, revisit crops, and render again from the original image.
SwiftUIPhotosCropView.SwiftUICropView for a custom interface, with EditingStack managing the editing state, history, and rendering.ParametricDocumentCodec.UIImage, data, file URLs, or remote URLs through ImageProvider.Choose the product that matches what you want to build:
| Product | Purpose | Platforms |
|---|---|---|
BrightroomUI | Photos-style editor and reusable SwiftUI editing components | iOS 17+ |
BrightroomEngine | Image loading, editing state and history, and asynchronous rendering | iOS 17+ |
BrightroomParametric | Typed features, masks, document persistence, and Core Image rendering, with no third-party dependencies | iOS 17+, macOS 14+ |
The Swift package requires Swift 6.3 or later and uses Swift 6 language mode. Use an Xcode toolchain that includes Swift 6.3 or later. The built-in UI uses UIKit internally; UIKit apps can present it through UIHostingController.
Add https://github.com/FluidGroup/Brightroom.git in Xcode's package dependencies, or declare it in your Package.swift:
dependencies: [
.package(url: "https://github.com/FluidGroup/Brightroom.git", from: "5.0.0")
]
For a photo editor, add BrightroomUI and BrightroomEngine to your app target. In a Swift package target, use:
.target(
name: "MyApp",
dependencies: [
.product(name: "BrightroomUI", package: "Brightroom"),
.product(name: "BrightroomEngine", package: "Brightroom"),
]
)
For processing without the editor, add BrightroomParametric instead.
EditingStack owns an editing session. PhotosCropEditingModel connects that session to the built-in editor's crop, filter, adjustment, and blur controls. Keep both instances alive for the lifetime of the editor.
import BrightroomEngine
import BrightroomUI
import SwiftUI
import UIKit
/// Edits one image and delivers the exported image or a rendering error.
@MainActor
struct PhotoEditor: View {
@Environment(\.dismiss) private var dismiss
@State private var editingStack: EditingStack
@State private var editingModel: PhotosCropEditingModel
@State private var isExporting = false
private let onComplete: (Result<UIImage, Error>) -> Void
init(
image: UIImage,
onComplete: @escaping (Result<UIImage, Error>) -> Void
) {
let stack = EditingStack(imageProvider: .init(image: image))
_editingStack = State(initialValue: stack)
_editingModel = State(initialValue: PhotosCropEditingModel(editingStack: stack))
self.onComplete = onComplete
}
var body: some View {
SwiftUIPhotosCropView(
editingModel: editingModel,
onDone: {
guard !isExporting else { return }
isExporting = true
Task {
defer { isExporting = false }
do {
let rendered = try await editingStack.makeRenderer().render()
onComplete(.success(try rendered.uiImage))
} catch {
onComplete(.failure(error))
}
}
},
onCancel: { dismiss() }
)
.disabled(isExporting)
.overlay {
if isExporting {
ProgressView()
}
}
}
}
The editor starts image preparation automatically. Handle the result and dismiss the editor from onComplete when appropriate. Create a new editor session when selecting a different source image.
Customize aspect ratios and filter presets through SwiftUIPhotosCropView.Options, and button labels through LocalizedStrings. For a custom layout and controls, start with SwiftUICropView. See the SwiftUI demo for complete integrations.
To export directly to disk, pass a file output to the renderer. This avoids creating a full-resolution output bitmap in memory:
let rendered = try await editingStack.makeRenderer().render(
options: .init(
output: .file(url: outputURL, fileType: .heif(quality: 0.9))
)
)
Choose a writable outputURL for the destination file. The result exposes fileURL and thumbnail(maxPixelSize:). Requesting cgImage or uiImage from a file-backed result decodes the full image back into memory.
BrightroomParametric stores edits in a Swift value tree. Features are evaluated in order, and each feature receives the preceding feature's output. The source image stays separate from the document.
import BrightroomParametric
import CoreImage
/// Builds an exposure-adjusted image recipe without modifying the source.
func makeAdjustedImage(from source: CIImage) throws -> CIImage {
let document = EditingDocument(
mainTree: MainTree(features: [
.effect(ExposureFeature(value: 0.5))
])
)
return try ParametricImageRenderer().makeImage(
from: source,
document: document
)
}
The returned CIImage is a lazy recipe. Use a CIContext to materialize it, or use ParametricExportRenderer to render a document to an image or file.
ImageEffectFeatureType to add a custom effect.EffectPipeline and a MaskTree in a LocalAdjustmentFeature to apply effects selectively.ParametricDocumentCodec to save and restore documents. Custom persisted features also conform to PersistableFeature and must be registered with the codec.ParametricVideoRenderer to create an AVMutableVideoComposition for playback or export through AVFoundation.Clone the repository with its submodules and open the development project:
git clone --recurse-submodules https://github.com/FluidGroup/Brightroom.git
cd Brightroom
open Dev/Brightroom.xcodeproj
Select a scheme and a matching run destination in Xcode. To build the iOS demo from the command line:
xcodebuild \
-project Dev/Brightroom.xcodeproj \
-scheme SwiftUIDemo \
-destination 'generic/platform=iOS Simulator' \
build
Support Brightroom on GitHub Sponsors or buy me a coffee.
Brightroom is available under the MIT license.
129 followers · starred Oct 2024
7,873 followers · starred Nov 2018
52 followers · starred Nov 2018
378 followers · starred Nov 2018
📷 A composable image editor using Core Image and Metal.
Swift
3,679
666 commits
updated Sep 27, 2026
A composable image editor powered by Core Image and Metal.
Build a photo editor with the included Photos-style UI, compose your own editing interface, or use the rendering engine on its own. Brightroom keeps edits as parameters, so you can change adjustments, revisit crops, and render again from the original image.
SwiftUIPhotosCropView.SwiftUICropView for a custom interface, with EditingStack managing the editing state, history, and rendering.ParametricDocumentCodec.UIImage, data, file URLs, or remote URLs through ImageProvider.Choose the product that matches what you want to build:
| Product | Purpose | Platforms |
|---|---|---|
BrightroomUI | Photos-style editor and reusable SwiftUI editing components | iOS 17+ |
BrightroomEngine | Image loading, editing state and history, and asynchronous rendering | iOS 17+ |
BrightroomParametric | Typed features, masks, document persistence, and Core Image rendering, with no third-party dependencies | iOS 17+, macOS 14+ |
The Swift package requires Swift 6.3 or later and uses Swift 6 language mode. Use an Xcode toolchain that includes Swift 6.3 or later. The built-in UI uses UIKit internally; UIKit apps can present it through UIHostingController.
Add https://github.com/FluidGroup/Brightroom.git in Xcode's package dependencies, or declare it in your Package.swift:
dependencies: [
.package(url: "https://github.com/FluidGroup/Brightroom.git", from: "5.0.0")
]
For a photo editor, add BrightroomUI and BrightroomEngine to your app target. In a Swift package target, use:
.target(
name: "MyApp",
dependencies: [
.product(name: "BrightroomUI", package: "Brightroom"),
.product(name: "BrightroomEngine", package: "Brightroom"),
]
)
For processing without the editor, add BrightroomParametric instead.
EditingStack owns an editing session. PhotosCropEditingModel connects that session to the built-in editor's crop, filter, adjustment, and blur controls. Keep both instances alive for the lifetime of the editor.
import BrightroomEngine
import BrightroomUI
import SwiftUI
import UIKit
/// Edits one image and delivers the exported image or a rendering error.
@MainActor
struct PhotoEditor: View {
@Environment(\.dismiss) private var dismiss
@State private var editingStack: EditingStack
@State private var editingModel: PhotosCropEditingModel
@State private var isExporting = false
private let onComplete: (Result<UIImage, Error>) -> Void
init(
image: UIImage,
onComplete: @escaping (Result<UIImage, Error>) -> Void
) {
let stack = EditingStack(imageProvider: .init(image: image))
_editingStack = State(initialValue: stack)
_editingModel = State(initialValue: PhotosCropEditingModel(editingStack: stack))
self.onComplete = onComplete
}
var body: some View {
SwiftUIPhotosCropView(
editingModel: editingModel,
onDone: {
guard !isExporting else { return }
isExporting = true
Task {
defer { isExporting = false }
do {
let rendered = try await editingStack.makeRenderer().render()
onComplete(.success(try rendered.uiImage))
} catch {
onComplete(.failure(error))
}
}
},
onCancel: { dismiss() }
)
.disabled(isExporting)
.overlay {
if isExporting {
ProgressView()
}
}
}
}
The editor starts image preparation automatically. Handle the result and dismiss the editor from onComplete when appropriate. Create a new editor session when selecting a different source image.
Customize aspect ratios and filter presets through SwiftUIPhotosCropView.Options, and button labels through LocalizedStrings. For a custom layout and controls, start with SwiftUICropView. See the SwiftUI demo for complete integrations.
To export directly to disk, pass a file output to the renderer. This avoids creating a full-resolution output bitmap in memory:
let rendered = try await editingStack.makeRenderer().render(
options: .init(
output: .file(url: outputURL, fileType: .heif(quality: 0.9))
)
)
Choose a writable outputURL for the destination file. The result exposes fileURL and thumbnail(maxPixelSize:). Requesting cgImage or uiImage from a file-backed result decodes the full image back into memory.
BrightroomParametric stores edits in a Swift value tree. Features are evaluated in order, and each feature receives the preceding feature's output. The source image stays separate from the document.
import BrightroomParametric
import CoreImage
/// Builds an exposure-adjusted image recipe without modifying the source.
func makeAdjustedImage(from source: CIImage) throws -> CIImage {
let document = EditingDocument(
mainTree: MainTree(features: [
.effect(ExposureFeature(value: 0.5))
])
)
return try ParametricImageRenderer().makeImage(
from: source,
document: document
)
}
The returned CIImage is a lazy recipe. Use a CIContext to materialize it, or use ParametricExportRenderer to render a document to an image or file.
ImageEffectFeatureType to add a custom effect.EffectPipeline and a MaskTree in a LocalAdjustmentFeature to apply effects selectively.ParametricDocumentCodec to save and restore documents. Custom persisted features also conform to PersistableFeature and must be registered with the codec.ParametricVideoRenderer to create an AVMutableVideoComposition for playback or export through AVFoundation.Clone the repository with its submodules and open the development project:
git clone --recurse-submodules https://github.com/FluidGroup/Brightroom.git
cd Brightroom
open Dev/Brightroom.xcodeproj
Select a scheme and a matching run destination in Xcode. To build the iOS demo from the command line:
xcodebuild \
-project Dev/Brightroom.xcodeproj \
-scheme SwiftUIDemo \
-destination 'generic/platform=iOS Simulator' \
build
Support Brightroom on GitHub Sponsors or buy me a coffee.
Brightroom is available under the MIT license.
129 followers · starred Oct 2024
7,873 followers · starred Nov 2018
52 followers · starred Nov 2018
378 followers · starred Nov 2018