diff --git a/ios/CVPixelBufferGetColors.swift b/ios/CVPixelBufferGetColors.swift new file mode 100644 index 0000000..843185f --- /dev/null +++ b/ios/CVPixelBufferGetColors.swift @@ -0,0 +1,310 @@ +// +// CVPixelBufferGetColors.swift +// Colorwaver +// +// Created by Marc Rousavy on 22.09.21. +// + + +// Inspired by: +// +// UIImageColors.swift +// https://github.com/jathu/UIImageColors +// +// Created by Jathu Satkunarajah (@jathu) on 2015-06-11 - Toronto +// + +import Foundation + +import UIKit + +public struct CVPixelBufferColors { + 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 +} + + +/* + 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 CVPixelBuffer { + + private func resizeForUIImageColors(newSize: CGSize) -> CVPixelBuffer? { + UIGraphicsBeginImageContextWithOptions(newSize, false, 0) + defer { + UIGraphicsEndImageContext() + } + //self.draw(in: CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height)) + return nil + } + + public func getColors(quality: UIImageColorsQuality = .high) -> CVPixelBufferColors? { + let width = CVPixelBufferGetWidth(self) + let height = CVPixelBufferGetHeight(self) + + let scaleDownSize: CGSize + if quality == .highest { + scaleDownSize = CGSize(width: width, height: height) + } else { + if width < height { + let ratio = CGFloat(height) / CGFloat(width) + scaleDownSize = CGSize(width: quality.rawValue / ratio, height: quality.rawValue) + } else { + let ratio = CGFloat(width) / CGFloat(height) + scaleDownSize = CGSize(width: quality.rawValue, height: quality.rawValue / ratio) + } + } + + guard let resizedImage = self.resizeForUIImageColors(newSize: scaleDownSize) else { return nil } + + let threshold = Int(Double(height) * 0.01) + var proposed: [Double] = [-1,-1,-1,-1] + + let imageColors = NSCountedSet(capacity: width * height) + for x in 0 ..< width { + for y in 0 ..< height { + let pixel: Int = ((width * y) + x) * 4 + if data[pixel+3] > 127 { + 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.. 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 + ) + } +} diff --git a/ios/Colorwaver.xcodeproj/project.pbxproj b/ios/Colorwaver.xcodeproj/project.pbxproj index f4cb19d..6843f17 100644 --- a/ios/Colorwaver.xcodeproj/project.pbxproj +++ b/ios/Colorwaver.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ B869228626DA27AC00186D7D /* PaletteFrameProcessorPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = B869228526DA27AC00186D7D /* PaletteFrameProcessorPlugin.m */; }; B869228826DA29AA00186D7D /* PaletteFrameProcessorPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = B869228726DA29AA00186D7D /* PaletteFrameProcessorPlugin.swift */; }; B89B4A6D26DA48ED0063A3DB /* UIColor+hexString.swift in Sources */ = {isa = PBXBuildFile; fileRef = B89B4A6C26DA48ED0063A3DB /* UIColor+hexString.swift */; }; + B8D9432D26FB77320087D73C /* CVPixelBufferGetColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D9432C26FB77320087D73C /* CVPixelBufferGetColors.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -51,6 +52,7 @@ B869228526DA27AC00186D7D /* PaletteFrameProcessorPlugin.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PaletteFrameProcessorPlugin.m; sourceTree = ""; }; B869228726DA29AA00186D7D /* PaletteFrameProcessorPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaletteFrameProcessorPlugin.swift; sourceTree = ""; }; B89B4A6C26DA48ED0063A3DB /* UIColor+hexString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+hexString.swift"; sourceTree = ""; }; + B8D9432C26FB77320087D73C /* CVPixelBufferGetColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CVPixelBufferGetColors.swift; sourceTree = ""; }; DDF131CE188414F6F7234528 /* Pods-Colorwaver-ColorwaverTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Colorwaver-ColorwaverTests.release.xcconfig"; path = "Target Support Files/Pods-Colorwaver-ColorwaverTests/Pods-Colorwaver-ColorwaverTests.release.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -117,6 +119,7 @@ B869228526DA27AC00186D7D /* PaletteFrameProcessorPlugin.m */, B869228726DA29AA00186D7D /* PaletteFrameProcessorPlugin.swift */, B89B4A6C26DA48ED0063A3DB /* UIColor+hexString.swift */, + B8D9432C26FB77320087D73C /* CVPixelBufferGetColors.swift */, ); name = Colorwaver; sourceTree = ""; @@ -221,9 +224,7 @@ TestTargetID = 13B07F861A680F5B00A75B9A; }; 13B07F861A680F5B00A75B9A = { - DevelopmentTeam = CJW62Q77E7; LastSwiftMigration = 1250; - ProvisioningStyle = Automatic; }; }; }; @@ -395,6 +396,7 @@ B89B4A6D26DA48ED0063A3DB /* UIColor+hexString.swift in Sources */, 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, B869228826DA29AA00186D7D /* PaletteFrameProcessorPlugin.swift in Sources */, + B8D9432D26FB77320087D73C /* CVPixelBufferGetColors.swift in Sources */, B869228626DA27AC00186D7D /* PaletteFrameProcessorPlugin.m in Sources */, B869228426DA1F9100186D7D /* File.swift in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, @@ -475,6 +477,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = CJW62Q77E7; @@ -491,7 +494,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.mrousavy.colorwaver; PRODUCT_NAME = Colorwaver; - PROVISIONING_PROFILE_SPECIFIER = "match Development com.mrousavy.colorwaver"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Colorwaver-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.2; @@ -505,6 +508,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = CJW62Q77E7; @@ -520,7 +524,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.mrousavy.colorwaver; PRODUCT_NAME = Colorwaver; - PROVISIONING_PROFILE_SPECIFIER = "match Development com.mrousavy.colorwaver"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Colorwaver-Bridging-Header.h"; SWIFT_VERSION = 5.2; VERSIONING_SYSTEM = "apple-generic";