Skip to content

Instantly share code, notes, and snippets.

@jonahaung
Last active May 21, 2020 04:58
Show Gist options
  • Save jonahaung/f64075661c03f3eda5ea7db2d5592676 to your computer and use it in GitHub Desktop.
Save jonahaung/f64075661c03f3eda5ea7db2d5592676 to your computer and use it in GitHub Desktop.
//
// UIImageColors.swift
// Myanmar Lens
//
// Created by Aung Ko Min on 13/1/20.
// Copyright © 2020 Aung Ko Min. All rights reserved.
//
#if os(OSX)
import AppKit
public typealias UIImage = NSImage
public typealias UIColor = NSColor
#else
import UIKit
#endif
public struct UIImageColors {
public var background: UIColor!
public var primary: UIColor!
public var secondary: UIColor!
public var detail: UIColor!
public init(background: UIColor, primary: UIColor, secondary: UIColor, detail: UIColor) {
self.background = background
self.primary = primary
self.secondary = secondary
self.detail = detail
}
}
public enum UIImageColorsQuality: CGFloat {
case lowest = 50 // 50px
case low = 100 // 100px
case high = 250 // 250px
case highest = 0 // No scale
}
fileprivate struct UIImageColorsCounter {
let color: Double
let count: Int
init(color: Double, count: Int) {
self.color = color
self.count = count
}
}
/*
Extension on double that replicates UIColor methods. We DO NOT want these
exposed outside of the library because they don't make sense outside of the
context of UIImageColors.
*/
fileprivate extension Double {
private var r: Double {
return fmod(floor(self/1000000),1000000)
}
private var g: Double {
return fmod(floor(self/1000),1000)
}
private var b: Double {
return fmod(self,1000)
}
var isDarkColor: Bool {
return (r*0.2126) + (g*0.7152) + (b*0.0722) < 127.5
}
var isBlackOrWhite: Bool {
return (r > 232 && g > 232 && b > 232) || (r < 23 && g < 23 && b < 23)
}
func isDistinct(_ other: Double) -> Bool {
let _r = self.r
let _g = self.g
let _b = self.b
let o_r = other.r
let o_g = other.g
let o_b = other.b
return (fabs(_r-o_r) > 63.75 || fabs(_g-o_g) > 63.75 || fabs(_b-o_b) > 63.75)
&& !(fabs(_r-_g) < 7.65 && fabs(_r-_b) < 7.65 && fabs(o_r-o_g) < 7.65 && fabs(o_r-o_b) < 7.65)
}
func with(minSaturation: Double) -> Double {
// Ref: https://en.wikipedia.org/wiki/HSL_and_HSV
// Convert RGB to HSV
let _r = r/255
let _g = g/255
let _b = b/255
var H, S, V: Double
let M = fmax(_r,fmax(_g, _b))
var C = M-fmin(_r,fmin(_g, _b))
V = M
S = V == 0 ? 0:C/V
if minSaturation <= S {
return self
}
if C == 0 {
H = 0
} else if _r == M {
H = fmod((_g-_b)/C, 6)
} else if _g == M {
H = 2+((_b-_r)/C)
} else {
H = 4+((_r-_g)/C)
}
if H < 0 {
H += 6
}
// Back to RGB
C = V*minSaturation
let X = C*(1-fabs(fmod(H,2)-1))
var R, G, B: Double
switch H {
case 0...1:
R = C
G = X
B = 0
case 1...2:
R = X
G = C
B = 0
case 2...3:
R = 0
G = C
B = X
case 3...4:
R = 0
G = X
B = C
case 4...5:
R = X
G = 0
B = C
case 5..<6:
R = C
G = 0
B = X
default:
R = 0
G = 0
B = 0
}
let m = V-C
return (floor((R + m)*255)*1000000)+(floor((G + m)*255)*1000)+floor((B + m)*255)
}
func isContrasting(_ color: Double) -> Bool {
let bgLum = (0.2126*r)+(0.7152*g)+(0.0722*b)+12.75
let fgLum = (0.2126*color.r)+(0.7152*color.g)+(0.0722*color.b)+12.75
if bgLum > fgLum {
return 1.6 < bgLum/fgLum
} else {
return 1.6 < fgLum/bgLum
}
}
var uicolor: UIColor {
return UIColor(red: CGFloat(r)/255, green: CGFloat(g)/255, blue: CGFloat(b)/255, alpha: 1)
}
var pretty: String {
return "\(Int(self.r)), \(Int(self.g)), \(Int(self.b))"
}
}
extension UIImage {
#if os(OSX)
private func resizeForUIImageColors(newSize: CGSize) -> UIImage? {
let frame = CGRect(origin: .zero, size: newSize)
guard let representation = bestRepresentation(for: frame, context: nil, hints: nil) else {
return nil
}
let result = NSImage(size: newSize, flipped: false, drawingHandler: { (_) -> Bool in
return representation.draw(in: frame)
})
return result
}
#else
private func resizeForUIImageColors(newSize: CGSize) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(newSize, false, 0)
defer {
UIGraphicsEndImageContext()
}
self.draw(in: CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height))
guard let result = UIGraphicsGetImageFromCurrentImageContext() else {
fatalError("UIImageColors.resizeForUIImageColors failed: UIGraphicsGetImageFromCurrentImageContext returned nil.")
}
return result
}
#endif
public func getColors(quality: UIImageColorsQuality = .high, _ completion: @escaping (UIImageColors?) -> Void) {
DispatchQueue.global().async {
let result = self.getColors(quality: quality)
DispatchQueue.main.async {
completion(result)
}
}
}
public func getColors(quality: UIImageColorsQuality = .high) -> UIImageColors? {
var scaleDownSize: CGSize = self.size
if quality != .highest {
if self.size.width < self.size.height {
let ratio = self.size.height/self.size.width
scaleDownSize = CGSize(width: quality.rawValue/ratio, height: quality.rawValue)
} else {
let ratio = self.size.width/self.size.height
scaleDownSize = CGSize(width: quality.rawValue, height: quality.rawValue/ratio)
}
}
guard let resizedImage = self.resizeForUIImageColors(newSize: scaleDownSize) else { return nil }
#if os(OSX)
guard let cgImage = resizedImage.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil }
#else
guard let cgImage = resizedImage.cgImage else { return nil }
#endif
let width: Int = cgImage.width
let height: Int = cgImage.height
let threshold = Int(CGFloat(height)*0.01)
var proposed: [Double] = [-1,-1,-1,-1]
guard let data = CFDataGetBytePtr(cgImage.dataProvider!.data) else {
fatalError("UIImageColors.getColors failed: could not get cgImage data.")
}
let imageColors = NSCountedSet(capacity: width*height)
for x in 0..<width {
for y in 0..<height {
let pixel: Int = ((width * y) + x) * 4
if 127 <= data[pixel+3] {
imageColors.add((Double(data[pixel+2])*1000000)+(Double(data[pixel+1])*1000)+(Double(data[pixel])))
}
}
}
let sortedColorComparator: Comparator = { (main, other) -> ComparisonResult in
let m = main as! UIImageColorsCounter, o = other as! UIImageColorsCounter
if m.count < o.count {
return .orderedDescending
} else if m.count == o.count {
return .orderedSame
} else {
return .orderedAscending
}
}
var enumerator = imageColors.objectEnumerator()
var sortedColors = NSMutableArray(capacity: imageColors.count)
while let K = enumerator.nextObject() as? Double {
let C = imageColors.count(for: K)
if threshold < C {
sortedColors.add(UIImageColorsCounter(color: K, count: C))
}
}
sortedColors.sort(comparator: sortedColorComparator)
var proposedEdgeColor: UIImageColorsCounter
if 0 < sortedColors.count {
proposedEdgeColor = sortedColors.object(at: 0) as! UIImageColorsCounter
} else {
proposedEdgeColor = UIImageColorsCounter(color: 0, count: 1)
}
if proposedEdgeColor.color.isBlackOrWhite && 0 < sortedColors.count {
for i in 1..<sortedColors.count {
let nextProposedEdgeColor = sortedColors.object(at: i) as! UIImageColorsCounter
if Double(nextProposedEdgeColor.count)/Double(proposedEdgeColor.count) > 0.3 {
if !nextProposedEdgeColor.color.isBlackOrWhite {
proposedEdgeColor = nextProposedEdgeColor
break
}
} else {
break
}
}
}
proposed[0] = proposedEdgeColor.color
enumerator = imageColors.objectEnumerator()
sortedColors.removeAllObjects()
sortedColors = NSMutableArray(capacity: imageColors.count)
let findDarkTextColor = !proposed[0].isDarkColor
while var K = enumerator.nextObject() as? Double {
K = K.with(minSaturation: 0.15)
if K.isDarkColor == findDarkTextColor {
let C = imageColors.count(for: K)
sortedColors.add(UIImageColorsCounter(color: K, count: C))
}
}
sortedColors.sort(comparator: sortedColorComparator)
for color in sortedColors {
let color = (color as! UIImageColorsCounter).color
if proposed[1] == -1 {
if color.isContrasting(proposed[0]) {
proposed[1] = color
}
} else if proposed[2] == -1 {
if !color.isContrasting(proposed[0]) || !proposed[1].isDistinct(color) {
continue
}
proposed[2] = color
} else if proposed[3] == -1 {
if !color.isContrasting(proposed[0]) || !proposed[2].isDistinct(color) || !proposed[1].isDistinct(color) {
continue
}
proposed[3] = color
break
}
}
let isDarkBackground = proposed[0].isDarkColor
for i in 1...3 {
if proposed[i] == -1 {
proposed[i] = isDarkBackground ? 255255255:0
}
}
return UIImageColors(
background: proposed[0].uicolor,
primary: proposed[1].uicolor,
secondary: proposed[2].uicolor,
detail: proposed[3].uicolor
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment