mirror of
https://github.com/Ranchero-Software/NetNewsWire.git
synced 2024-12-22 23:58:36 +01:00
Change the luminance algorithm so that we don't miss images in unexpected formats. Fixes #2967
This commit is contained in:
parent
0d5de9c325
commit
1874e0c7d2
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user