This repository has been archived by the owner. It is now read-only.
Permalink
278 lines (231 sloc)
9.94 KB
| // | |
| // UIImageColors.swift | |
| // https://github.com/jathu/UIImageColors | |
| // | |
| // Created by Jathu Satkunarajah (@jathu) on 2015-06-11 - Toronto | |
| // Original Cocoa version by Panic Inc. - Portland | |
| // | |
| import UIKit | |
| public struct UIImageColors { | |
| public var background: UIColor! | |
| public var primary: UIColor! | |
| public var secondary: UIColor! | |
| public var detail: UIColor! | |
| } | |
| class PCCountedColor { | |
| let color: UIColor | |
| let count: Int | |
| init(color: UIColor, count: Int) { | |
| self.color = color | |
| self.count = count | |
| } | |
| } | |
| extension CGColor { | |
| var components: [CGFloat] { | |
| get { | |
| var red = CGFloat() | |
| var green = CGFloat() | |
| var blue = CGFloat() | |
| var alpha = CGFloat() | |
| UIColor(cgColor: self).getRed(&red, green: &green, blue: &blue, alpha: &alpha) | |
| return [red,green,blue,alpha] | |
| } | |
| } | |
| } | |
| extension UIColor { | |
| var isDarkColor: Bool { | |
| let RGB = self.cgColor.components | |
| return (0.2126 * RGB[0] + 0.7152 * RGB[1] + 0.0722 * RGB[2]) < 0.5 | |
| } | |
| var isBlackOrWhite: Bool { | |
| let RGB = self.cgColor.components | |
| return (RGB[0] > 0.91 && RGB[1] > 0.91 && RGB[2] > 0.91) || (RGB[0] < 0.09 && RGB[1] < 0.09 && RGB[2] < 0.09) | |
| } | |
| func isDistinct(compareColor: UIColor) -> Bool { | |
| let bg = self.cgColor.components | |
| let fg = compareColor.cgColor.components | |
| let threshold: CGFloat = 0.25 | |
| if fabs(bg[0] - fg[0]) > threshold || fabs(bg[1] - fg[1]) > threshold || fabs(bg[2] - fg[2]) > threshold { | |
| if fabs(bg[0] - bg[1]) < 0.03 && fabs(bg[0] - bg[2]) < 0.03 { | |
| if fabs(fg[0] - fg[1]) < 0.03 && fabs(fg[0] - fg[2]) < 0.03 { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| return false | |
| } | |
| func colorWithMinimumSaturation(minSaturation: CGFloat) -> UIColor { | |
| var hue: CGFloat = 0.0 | |
| var saturation: CGFloat = 0.0 | |
| var brightness: CGFloat = 0.0 | |
| var alpha: CGFloat = 0.0 | |
| self.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) | |
| if saturation < minSaturation { | |
| return UIColor(hue: hue, saturation: minSaturation, brightness: brightness, alpha: alpha) | |
| } else { | |
| return self | |
| } | |
| } | |
| func isContrastingColor(compareColor: UIColor) -> Bool { | |
| let bg = self.cgColor.components | |
| let fg = compareColor.cgColor.components | |
| let bgLum = 0.2126 * bg[0] + 0.7152 * bg[1] + 0.0722 * bg[2] | |
| let fgLum = 0.2126 * fg[0] + 0.7152 * fg[1] + 0.0722 * fg[2] | |
| let bgGreater = bgLum > fgLum | |
| let nom = bgGreater ? bgLum : fgLum | |
| let denom = bgGreater ? fgLum : bgLum | |
| let contrast = (nom + 0.05) / (denom + 0.05) | |
| return 1.6 < contrast | |
| } | |
| } | |
| extension UIImage { | |
| private func resizeForUIImageColors(newSize: CGSize) -> UIImage { | |
| UIGraphicsBeginImageContextWithOptions(newSize, false, 0) | |
| defer { | |
| UIGraphicsEndImageContext() | |
| } | |
| self.draw(in: CGRect(width: newSize.width, height: newSize.height)) | |
| guard let result = UIGraphicsGetImageFromCurrentImageContext() else { | |
| fatalError("UIImageColors.resizeForUIImageColors failed: UIGraphicsGetImageFromCurrentImageContext returned nil") | |
| } | |
| return result | |
| } | |
| /** | |
| Get `UIImageColors` from the image asynchronously (in background thread). | |
| Discussion: Use smaller sizes for better performance at the cost of quality colors. Use larger sizes for better color sampling and quality at the cost of performance. | |
| - parameter scaleDownSize: Downscale size of image for sampling, if `CGSize.zero` is provided, the sample image is rescaled to a width of 250px and the aspect ratio height. | |
| - parameter completionHandler: `UIImageColors` for this image. | |
| */ | |
| public func getColors(scaleDownSize: CGSize = .zero, completionHandler: @escaping (UIImageColors) -> Void) { | |
| DispatchQueue.global().async { | |
| let result = self.getColors(scaleDownSize: scaleDownSize) | |
| DispatchQueue.main.async { | |
| completionHandler(result) | |
| } | |
| } | |
| } | |
| /** | |
| Get `UIImageColors` from the image synchronously (in main thread). | |
| Discussion: Use smaller sizes for better performance at the cost of quality colors. Use larger sizes for better color sampling and quality at the cost of performance. | |
| - parameter scaleDownSize: Downscale size of image for sampling, if `CGSize.zero` is provided, the sample image is rescaled to a width of 250px and the aspect ratio height. | |
| - returns: `UIImageColors` for this image. | |
| */ | |
| public func getColors(scaleDownSize: CGSize = .zero) -> UIImageColors { | |
| var scaleDownSize = scaleDownSize | |
| if scaleDownSize == .zero { | |
| let ratio = self.size.width/self.size.height | |
| let r_width: CGFloat = 250 | |
| scaleDownSize = CGSize(width: r_width, height: r_width/ratio) | |
| } | |
| var result = UIImageColors() | |
| let cgImage = self.resizeForUIImageColors(newSize: scaleDownSize).cgImage! | |
| let width: Int = cgImage.width | |
| let height: Int = cgImage.height | |
| let blackColor = UIColor(red: 0, green: 0, blue: 0, alpha: 1) | |
| let whiteColor = UIColor(red: 1, green: 1, blue: 1, alpha: 1) | |
| let randomColorsThreshold = Int(CGFloat(height)*0.01) | |
| let sortedColorComparator: Comparator = { (main, other) -> ComparisonResult in | |
| let m = main as! PCCountedColor, o = other as! PCCountedColor | |
| if m.count < o.count { | |
| return .orderedDescending | |
| } else if m.count == o.count { | |
| return .orderedSame | |
| } else { | |
| return .orderedAscending | |
| } | |
| } | |
| guard let data = CFDataGetBytePtr(cgImage.dataProvider!.data) else { | |
| fatalError("UIImageColors.getColors failed: could not get cgImage data") | |
| } | |
| // Filter out and collect pixels from image | |
| let imageColors = NSCountedSet(capacity: width * height) | |
| for x in 0..<width { | |
| for y in 0..<height { | |
| let pixel: Int = ((width * y) + x) * 4 | |
| // Only consider pixels with 50% opacity or higher | |
| if 127 <= data[pixel+3] { | |
| imageColors.add(UIColor( | |
| red: CGFloat(data[pixel+2])/255, | |
| green: CGFloat(data[pixel+1])/255, | |
| blue: CGFloat(data[pixel])/255, | |
| alpha: 1.0 | |
| )) | |
| } | |
| } | |
| } | |
| // Get background color | |
| var enumerator = imageColors.objectEnumerator() | |
| var sortedColors = NSMutableArray(capacity: imageColors.count) | |
| while let kolor = enumerator.nextObject() as? UIColor { | |
| let colorCount = imageColors.count(for: kolor) | |
| if randomColorsThreshold < colorCount { | |
| sortedColors.add(PCCountedColor(color: kolor, count: colorCount)) | |
| } | |
| } | |
| sortedColors.sort(comparator: sortedColorComparator) | |
| var proposedEdgeColor: PCCountedColor | |
| if 0 < sortedColors.count { | |
| proposedEdgeColor = sortedColors.object(at: 0) as! PCCountedColor | |
| } else { | |
| proposedEdgeColor = PCCountedColor(color: blackColor, count: 1) | |
| } | |
| if proposedEdgeColor.color.isBlackOrWhite && 0 < sortedColors.count { | |
| for i in 1..<sortedColors.count { | |
| let nextProposedEdgeColor = sortedColors.object(at: i) as! PCCountedColor | |
| if (CGFloat(nextProposedEdgeColor.count)/CGFloat(proposedEdgeColor.count)) > 0.3 { | |
| if !nextProposedEdgeColor.color.isBlackOrWhite { | |
| proposedEdgeColor = nextProposedEdgeColor | |
| break | |
| } | |
| } else { | |
| break | |
| } | |
| } | |
| } | |
| result.background = proposedEdgeColor.color | |
| // Get foreground colors | |
| enumerator = imageColors.objectEnumerator() | |
| sortedColors.removeAllObjects() | |
| sortedColors = NSMutableArray(capacity: imageColors.count) | |
| let findDarkTextColor = !result.background.isDarkColor | |
| while var kolor = enumerator.nextObject() as? UIColor { | |
| kolor = kolor.colorWithMinimumSaturation(minSaturation: 0.15) | |
| if kolor.isDarkColor == findDarkTextColor { | |
| let colorCount = imageColors.count(for: kolor) | |
| sortedColors.add(PCCountedColor(color: kolor, count: colorCount)) | |
| } | |
| } | |
| sortedColors.sort(comparator: sortedColorComparator) | |
| for curContainer in sortedColors { | |
| let kolor = (curContainer as! PCCountedColor).color | |
| if result.primary == nil { | |
| if kolor.isContrastingColor(compareColor: result.background) { | |
| result.primary = kolor | |
| } | |
| } else if result.secondary == nil { | |
| if !result.primary.isDistinct(compareColor: kolor) || !kolor.isContrastingColor(compareColor: result.background) { | |
| continue | |
| } | |
| result.secondary = kolor | |
| } else if result.detail == nil { | |
| if !result.secondary.isDistinct(compareColor: kolor) || !result.primary.isDistinct(compareColor: kolor) || !kolor.isContrastingColor(compareColor: result.background) { | |
| continue | |
| } | |
| result.detail = kolor | |
| break | |
| } | |
| } | |
| let isDarkBackgound = result.background.isDarkColor | |
| if result.primary == nil { | |
| result.primary = isDarkBackgound ? whiteColor:blackColor | |
| } | |
| if result.secondary == nil { | |
| result.secondary = isDarkBackgound ? whiteColor:blackColor | |
| } | |
| if result.detail == nil { | |
| result.detail = isDarkBackgound ? whiteColor:blackColor | |
| } | |
| return result | |
| } | |
| } |