Skip to content

Commit 099f290

Browse files
committed
Rework color averaging algorithm
1 parent 0d958d6 commit 099f290

File tree

3 files changed

+52
-122
lines changed

3 files changed

+52
-122
lines changed

Ice/MenuBar/MenuBarManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ final class MenuBarManager: ObservableObject {
261261

262262
guard
263263
let image,
264-
let color = image.averageColor(resolution: .low, options: .ignoreAlpha)
264+
let color = image.averageColor(makeOpaque: true)
265265
else {
266266
return
267267
}

Ice/UI/IceBar/IceBarColorManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ final class IceBarColorManager: ObservableObject {
118118

119119
guard
120120
let croppedImage = windowImage.cropping(to: cropRect),
121-
let averageColor = croppedImage.averageColor(resolution: .low)
121+
let averageColor = croppedImage.averageColor()
122122
else {
123123
colorInfo = nil
124124
return

Ice/Utilities/Extensions.swift

Lines changed: 50 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -70,156 +70,86 @@ extension CGError {
7070
// MARK: - CGImage
7171

7272
extension CGImage {
73-
/// Constants that determine the resolution of a color averaging algorithm.
74-
enum ColorAverageResolution {
75-
/// Low resolution, reducing accuracy, but increasing performance.
76-
case low
77-
/// Medium resolution, with nominal accuracy and performance.
78-
case medium
79-
/// High resolution, increasing accuracy, but reducing performance.
80-
case high
81-
}
82-
83-
/// Options that affect the output of a color averaging algorithm.
84-
struct ColorAverageOptions: OptionSet {
85-
let rawValue: Int
8673

87-
/// The alpha component of the result is ignored and replaced with a value of `1`.
88-
static let ignoreAlpha = ColorAverageOptions(rawValue: 1 << 0)
89-
}
90-
91-
/// A color component in the ARGB color space.
92-
private enum ARGBComponent: UInt32 {
93-
case alpha = 0x18
94-
case red = 0x10
95-
case green = 0x08
96-
case blue = 0x00
97-
}
74+
// MARK: Average Color
9875

9976
/// Computes and returns the average color of the image.
10077
///
10178
/// - Parameters:
102-
/// - resolution: The resolution of the algorithm.
103-
/// - options: Options that further specify how the average should be computed.
104-
/// - alphaThreshold: An alpha value below which pixels should be ignored. Pixels
105-
/// whose alpha component is less than this value are not used in the computation.
106-
func averageColor(
107-
resolution: ColorAverageResolution = .medium,
108-
options: ColorAverageOptions = [],
109-
alphaThreshold: CGFloat = 0.5
110-
) -> CGColor? {
111-
// Resize the image based on the resolution. Smaller images remove more pixels,
112-
// decreasing accuracy, but increasing performance.
113-
let size = switch resolution {
114-
case .low:
115-
CGSize(width: min(width, 10), height: min(height, 10))
116-
case .medium:
117-
CGSize(width: min(width, 50), height: min(height, 50))
118-
case .high:
119-
CGSize(width: min(width, 100), height: min(height, 100))
79+
/// - alphaThreshold: An alpha value below which pixels should be ignored. Pixels with
80+
/// an alpha component greater than or equal to this value contribute to the average.
81+
/// - makeOpaque: A Boolean value that indicates whether the resulting color should be
82+
/// made opaque, regardless of the alpha content of the image.
83+
func averageColor(alphaThreshold: CGFloat = 0.5, makeOpaque: Bool = false) -> CGColor? {
84+
func createPixelData(width: Int, height: Int) -> [UInt32]? {
85+
var data = [UInt32](repeating: 0, count: width * height)
86+
guard let context = CGContext(
87+
data: &data,
88+
width: width,
89+
height: height,
90+
bitsPerComponent: 8,
91+
bytesPerRow: width * 4,
92+
space: CGColorSpaceCreateDeviceRGB(),
93+
bitmapInfo: CGImageByteOrderInfo.order32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue
94+
) else {
95+
return nil
96+
}
97+
context.draw(self, in: CGRect(x: 0, y: 0, width: width, height: height))
98+
return data
12099
}
121100

122-
guard
123-
let context = createContext(size: size),
124-
let data = createImageData(context: context)
125-
else {
126-
return nil
101+
func computeComponent(shift: UInt32, pixel: UInt32) -> Int {
102+
return Int((pixel >> shift) & 255)
127103
}
128104

129-
let width = Int(size.width)
130-
let height = Int(size.height)
105+
// Resize the image for better performance.
106+
let width = min(width, 10)
107+
let height = min(height, 10)
131108

132-
// Convert the alpha threshold to an integer, multiplied by 255. Pixels with
133-
// an alpha component below this value are excluded from the average.
134-
let alphaThreshold = Int(alphaThreshold * 255)
109+
guard let pixelData = createPixelData(width: width, height: height) else {
110+
return nil
111+
}
135112

136-
// Start with a full pixel count. If any pixels are skipped, the count is
137-
// decremented accordingly.
138-
var pixelCount = width * height
113+
// Convert the alpha threshold to a valid component for comparison.
114+
let alphaThreshold = Int((alphaThreshold.clamped(to: 0...1) * 255).rounded(.toNearestOrAwayFromZero))
139115

140-
// Start with the totals zeroed out.
141-
var totalRed = 0
142-
var totalGreen = 0
143-
var totalBlue = 0
144-
var totalAlpha = 0
116+
var includedPixelCount = width * height
117+
var totals = (red: 0, green: 0, blue: 0, alpha: 0)
145118

146119
for column in 0..<width {
147120
for row in 0..<height {
148-
let pixel = data[(row * width) + column]
121+
let pixel = pixelData[(row * width) + column]
149122

150123
// Check alpha before computing other components.
151-
let alphaComponent = computeComponentValue(.alpha, for: pixel)
124+
let alphaComponent = computeComponent(shift: 24, pixel: pixel)
152125

153126
guard alphaComponent >= alphaThreshold else {
154-
pixelCount -= 1 // Don't include this pixel.
127+
includedPixelCount -= 1 // Don't include this pixel.
155128
continue
156129
}
157130

158-
let redComponent = computeComponentValue(.red, for: pixel)
159-
let greenComponent = computeComponentValue(.green, for: pixel)
160-
let blueComponent = computeComponentValue(.blue, for: pixel)
161-
162-
// Sum the red, green, blue, and alpha components.
163-
totalRed += redComponent
164-
totalGreen += greenComponent
165-
totalBlue += blueComponent
166-
totalAlpha += alphaComponent
131+
// Add the components to the totals.
132+
totals.red += computeComponent(shift: 16, pixel: pixel)
133+
totals.green += computeComponent(shift: 8, pixel: pixel)
134+
totals.blue += computeComponent(shift: 0, pixel: pixel)
135+
totals.alpha += alphaComponent
167136
}
168137
}
169138

170-
// Compute the averages of the summed components.
171-
let averageRed = CGFloat(totalRed) / CGFloat(pixelCount)
172-
let averageGreen = CGFloat(totalGreen) / CGFloat(pixelCount)
173-
let averageBlue = CGFloat(totalBlue) / CGFloat(pixelCount)
174-
let averageAlpha = CGFloat(totalAlpha) / CGFloat(pixelCount)
139+
// Multiply the included pixel count by 255 to convert the components
140+
// to their corresponding floating point values.
141+
let adjustedPixelCount = CGFloat(includedPixelCount * 255)
175142

176-
// Divide each component by 255 to convert to floating point.
177-
let red = averageRed / 255
178-
let green = averageGreen / 255
179-
let blue = averageBlue / 255
180-
let alpha = options.contains(.ignoreAlpha) ? 1 : averageAlpha / 255
181-
182-
return CGColor(red: red, green: green, blue: blue, alpha: alpha)
183-
}
184-
185-
/// Creates a bitmap context for resizing the image to the given size.
186-
private func createContext(size: CGSize) -> CGContext? {
187-
let width = Int(size.width)
188-
let height = Int(size.height)
189-
let bytesPerRow = width * 4
190-
let colorSpace = CGColorSpaceCreateDeviceRGB()
191-
let byteOrder = CGImageByteOrderInfo.order32Little.rawValue
192-
let alphaInfo = CGImageAlphaInfo.premultipliedFirst.rawValue
193-
return CGContext(
194-
data: nil,
195-
width: width,
196-
height: height,
197-
bitsPerComponent: 8,
198-
bytesPerRow: bytesPerRow,
199-
space: colorSpace,
200-
bitmapInfo: byteOrder | alphaInfo
143+
return CGColor(
144+
red: CGFloat(totals.red) / adjustedPixelCount,
145+
green: CGFloat(totals.green) / adjustedPixelCount,
146+
blue: CGFloat(totals.blue) / adjustedPixelCount,
147+
alpha: makeOpaque ? 1 : CGFloat(totals.alpha) / adjustedPixelCount
201148
)
202149
}
203150

204-
/// Draws the image into the given context and returns the raw data.
205-
private func createImageData(context: CGContext) -> UnsafeMutablePointer<UInt32>? {
206-
let rect = CGRect(x: 0, y: 0, width: context.width, height: context.height)
207-
context.draw(self, in: rect)
208-
guard let rawData = context.data else {
209-
return nil
210-
}
211-
return rawData.bindMemory(to: UInt32.self, capacity: context.width * context.height)
212-
}
213-
214-
/// Computes the value of a color component for the given pixel value.
215-
private func computeComponentValue(_ component: ARGBComponent, for pixel: UInt32) -> Int {
216-
return Int((pixel >> component.rawValue) & 255)
217-
}
218-
}
219-
220-
// MARK: - CGImage
151+
// MARK: Trim Transparent Pixels
221152

222-
extension CGImage {
223153
/// A context for handling transparency data in an image.
224154
private final class TransparencyContext {
225155
private let image: CGImage

0 commit comments

Comments
 (0)