diff --git a/Shared/Extensions/IconImage.swift b/Shared/Extensions/IconImage.swift index dd11e0933..e460e4b6b 100644 --- a/Shared/Extensions/IconImage.swift +++ b/Shared/Extensions/IconImage.swift @@ -63,47 +63,81 @@ final class IconImage { fileprivate enum ImageLuminanceType { case regular, bright, dark } + extension CGImage { func isBright() -> Bool { - guard let imageData = self.dataProvider?.data, let luminanceType = getLuminanceType(from: imageData) else { + guard let luminanceType = getLuminanceType() else { return false } return luminanceType == .bright } func isDark() -> Bool { - guard let imageData = self.dataProvider?.data, let luminanceType = getLuminanceType(from: imageData) else { + guard let luminanceType = getLuminanceType() else { return false } return luminanceType == .dark } - fileprivate func getLuminanceType(from data: CFData) -> ImageLuminanceType? { - guard let ptr = CFDataGetBytePtr(data) else { - return nil - } - - let length = CFDataGetLength(data) - var pixelCount = 0 + fileprivate func getLuminanceType() -> ImageLuminanceType? { + + // This has been rewritten with information from https://christianselig.com/2021/04/efficient-average-color/ + + // First, resize the image. We do this for two reasons, 1) less pixels to deal with means faster + // calculation and a resized image still has the "gist" of the colors, and 2) the image we're dealing + // with may come in any of a variety of color formats (CMYK, ARGB, RGBA, etc.) which complicates things, + // and redrawing it normalizes that into a base color format we can deal with. + // 40x40 is a good size to resize to still preserve quite a bit of detail but not have too many pixels + // to deal with. Aspect ratio is irrelevant for just finding average color. + let size = CGSize(width: 40, height: 40) + + let width = Int(size.width) + let height = Int(size.height) + let totalPixels = width * height + + let colorSpace = CGColorSpaceCreateDeviceRGB() + + // ARGB format + let bitmapInfo: UInt32 = CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue + + // 8 bits for each color channel, we're doing ARGB so 32 bits (4 bytes) total, and thus if the image is n pixels wide, + // and has 4 bytes per pixel, the total bytes per row is 4n. That gives us 2^8 = 256 color variations for each RGB channel + // or 256 * 256 * 256 = ~16.7M color options in total. That seems like a lot, but lots of HDR movies are in 10 bit, which + // is (2^10)^3 = 1 billion color options! + guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: colorSpace, bitmapInfo: bitmapInfo) else { return nil } + + // Draw our resized image + context.draw(self, in: CGRect(origin: .zero, size: size)) + + guard let pixelBuffer = context.data else { return nil } + + // Bind the pixel buffer's memory location to a pointer we can use/access + let pointer = pixelBuffer.bindMemory(to: UInt32.self, capacity: width * height) + var totalLuminance = 0.0 - for i in stride(from: 0, to: length, by: 4) { - - let r = ptr[i] - let g = ptr[i + 1] - let b = ptr[i + 2] - let a = ptr[i + 3] - let luminance = (0.299 * Double(r) + 0.587 * Double(g) + 0.114 * Double(b)) - - if Double(a) > 0 { + // Column of pixels in image + for x in 0 ..< width { + // Row of pixels in image + for y in 0 ..< height { + // To get the pixel location just think of the image as a grid of pixels, but stored as one long row + // rather than columns and rows, so for instance to map the pixel from the grid in the 15th row and 3 + // columns in to our "long row", we'd offset ourselves 15 times the width in pixels of the image, and + // then offset by the amount of columns + let pixel = pointer[(y * width) + x] + + let r = red(for: pixel) + let g = green(for: pixel) + let b = blue(for: pixel) + + let luminance = (0.299 * Double(r) + 0.587 * Double(g) + 0.114 * Double(b)) + totalLuminance += luminance - pixelCount += 1 } - } - let avgLuminance = totalLuminance / Double(pixelCount) + let avgLuminance = totalLuminance / Double(totalPixels) if totalLuminance == 0 || avgLuminance < 40 { return .dark } else if avgLuminance > 180 { @@ -113,6 +147,18 @@ extension CGImage { } } + private func red(for pixelData: UInt32) -> UInt8 { + return UInt8((pixelData >> 16) & 255) + } + + private func green(for pixelData: UInt32) -> UInt8 { + return UInt8((pixelData >> 8) & 255) + } + + private func blue(for pixelData: UInt32) -> UInt8 { + return UInt8((pixelData >> 0) & 255) + } + }