├── .gitignore ├── Framework ├── Palette.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── Palette.xcscheme │ └── xcuserdata │ │ └── galandezzz.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── Palette │ ├── Info.plist │ └── Palette.h ├── LICENSE ├── Palette.podspec ├── README.md └── Source ├── Color.swift ├── ColorConverter.swift ├── ColorCutQuantizer.swift ├── Data Structures ├── CountedSet.swift ├── Heap.swift └── PriorityQueue.swift ├── Palette.swift ├── PaletteBuilder.swift ├── PaletteFilter.swift ├── PaletteSwatch.swift ├── Target.swift ├── TargetBuilder.swift └── UIImage+Palette.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /Framework/Palette.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0F53B15822FF63E500039D92 /* TargetBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F53B15722FF63E500039D92 /* TargetBuilder.swift */; }; 11 | 0F53B15A22FF643700039D92 /* ColorConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F53B15922FF643700039D92 /* ColorConverter.swift */; }; 12 | 0FE76AE922FCD15F00F23EE6 /* Palette.h in Headers */ = {isa = PBXBuildFile; fileRef = 0FE76AE722FCD15F00F23EE6 /* Palette.h */; settings = {ATTRIBUTES = (Public, ); }; }; 13 | 0FE76AFB22FCD1CA00F23EE6 /* Palette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FE76AF022FCD1CA00F23EE6 /* Palette.swift */; }; 14 | 0FE76AFC22FCD1CA00F23EE6 /* PriorityQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FE76AF222FCD1CA00F23EE6 /* PriorityQueue.swift */; }; 15 | 0FE76AFD22FCD1CA00F23EE6 /* Heap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FE76AF322FCD1CA00F23EE6 /* Heap.swift */; }; 16 | 0FE76AFE22FCD1CA00F23EE6 /* CountedSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FE76AF422FCD1CA00F23EE6 /* CountedSet.swift */; }; 17 | 0FE76AFF22FCD1CA00F23EE6 /* PaletteSwatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FE76AF522FCD1CA00F23EE6 /* PaletteSwatch.swift */; }; 18 | 0FE76B0022FCD1CA00F23EE6 /* Target.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FE76AF622FCD1CA00F23EE6 /* Target.swift */; }; 19 | 0FE76B0122FCD1CA00F23EE6 /* PaletteFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FE76AF722FCD1CA00F23EE6 /* PaletteFilter.swift */; }; 20 | 0FE76B0222FCD1CA00F23EE6 /* ColorCutQuantizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FE76AF822FCD1CA00F23EE6 /* ColorCutQuantizer.swift */; }; 21 | 0FE76B0322FCD1CA00F23EE6 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FE76AF922FCD1CA00F23EE6 /* Color.swift */; }; 22 | 0FE76B0422FCD1CA00F23EE6 /* PaletteBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FE76AFA22FCD1CA00F23EE6 /* PaletteBuilder.swift */; }; 23 | 0FE76B0622FCD2C100F23EE6 /* UIImage+Palette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FE76B0522FCD2C100F23EE6 /* UIImage+Palette.swift */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | 0F53B15722FF63E500039D92 /* TargetBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetBuilder.swift; sourceTree = ""; }; 28 | 0F53B15922FF643700039D92 /* ColorConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorConverter.swift; sourceTree = ""; }; 29 | 0FE76AE422FCD15F00F23EE6 /* Palette.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Palette.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | 0FE76AE722FCD15F00F23EE6 /* Palette.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Palette.h; sourceTree = ""; }; 31 | 0FE76AE822FCD15F00F23EE6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 32 | 0FE76AF022FCD1CA00F23EE6 /* Palette.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Palette.swift; sourceTree = ""; }; 33 | 0FE76AF222FCD1CA00F23EE6 /* PriorityQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PriorityQueue.swift; sourceTree = ""; }; 34 | 0FE76AF322FCD1CA00F23EE6 /* Heap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Heap.swift; sourceTree = ""; }; 35 | 0FE76AF422FCD1CA00F23EE6 /* CountedSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountedSet.swift; sourceTree = ""; }; 36 | 0FE76AF522FCD1CA00F23EE6 /* PaletteSwatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaletteSwatch.swift; sourceTree = ""; }; 37 | 0FE76AF622FCD1CA00F23EE6 /* Target.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Target.swift; sourceTree = ""; }; 38 | 0FE76AF722FCD1CA00F23EE6 /* PaletteFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaletteFilter.swift; sourceTree = ""; }; 39 | 0FE76AF822FCD1CA00F23EE6 /* ColorCutQuantizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorCutQuantizer.swift; sourceTree = ""; }; 40 | 0FE76AF922FCD1CA00F23EE6 /* Color.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; 41 | 0FE76AFA22FCD1CA00F23EE6 /* PaletteBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaletteBuilder.swift; sourceTree = ""; }; 42 | 0FE76B0522FCD2C100F23EE6 /* UIImage+Palette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Palette.swift"; sourceTree = ""; }; 43 | /* End PBXFileReference section */ 44 | 45 | /* Begin PBXFrameworksBuildPhase section */ 46 | 0FE76AE122FCD15F00F23EE6 /* Frameworks */ = { 47 | isa = PBXFrameworksBuildPhase; 48 | buildActionMask = 2147483647; 49 | files = ( 50 | ); 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | /* End PBXFrameworksBuildPhase section */ 54 | 55 | /* Begin PBXGroup section */ 56 | 0FE76ADA22FCD15F00F23EE6 = { 57 | isa = PBXGroup; 58 | children = ( 59 | 0FE76AE622FCD15F00F23EE6 /* Palette */, 60 | 0FE76AE522FCD15F00F23EE6 /* Products */, 61 | ); 62 | sourceTree = ""; 63 | }; 64 | 0FE76AE522FCD15F00F23EE6 /* Products */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 0FE76AE422FCD15F00F23EE6 /* Palette.framework */, 68 | ); 69 | name = Products; 70 | sourceTree = ""; 71 | }; 72 | 0FE76AE622FCD15F00F23EE6 /* Palette */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | 0FE76AE722FCD15F00F23EE6 /* Palette.h */, 76 | 0FE76AE822FCD15F00F23EE6 /* Info.plist */, 77 | 0FE76AEF22FCD1CA00F23EE6 /* Source */, 78 | ); 79 | path = Palette; 80 | sourceTree = ""; 81 | }; 82 | 0FE76AEF22FCD1CA00F23EE6 /* Source */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 0FE76AF022FCD1CA00F23EE6 /* Palette.swift */, 86 | 0FE76AFA22FCD1CA00F23EE6 /* PaletteBuilder.swift */, 87 | 0FE76AF522FCD1CA00F23EE6 /* PaletteSwatch.swift */, 88 | 0FE76AF722FCD1CA00F23EE6 /* PaletteFilter.swift */, 89 | 0FE76AF622FCD1CA00F23EE6 /* Target.swift */, 90 | 0F53B15722FF63E500039D92 /* TargetBuilder.swift */, 91 | 0FE76AF822FCD1CA00F23EE6 /* ColorCutQuantizer.swift */, 92 | 0FE76AF922FCD1CA00F23EE6 /* Color.swift */, 93 | 0F53B15922FF643700039D92 /* ColorConverter.swift */, 94 | 0FE76B0522FCD2C100F23EE6 /* UIImage+Palette.swift */, 95 | 0FE76AF122FCD1CA00F23EE6 /* Data Structures */, 96 | ); 97 | name = Source; 98 | path = ../../Source; 99 | sourceTree = ""; 100 | }; 101 | 0FE76AF122FCD1CA00F23EE6 /* Data Structures */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 0FE76AF222FCD1CA00F23EE6 /* PriorityQueue.swift */, 105 | 0FE76AF322FCD1CA00F23EE6 /* Heap.swift */, 106 | 0FE76AF422FCD1CA00F23EE6 /* CountedSet.swift */, 107 | ); 108 | path = "Data Structures"; 109 | sourceTree = ""; 110 | }; 111 | /* End PBXGroup section */ 112 | 113 | /* Begin PBXHeadersBuildPhase section */ 114 | 0FE76ADF22FCD15F00F23EE6 /* Headers */ = { 115 | isa = PBXHeadersBuildPhase; 116 | buildActionMask = 2147483647; 117 | files = ( 118 | 0FE76AE922FCD15F00F23EE6 /* Palette.h in Headers */, 119 | ); 120 | runOnlyForDeploymentPostprocessing = 0; 121 | }; 122 | /* End PBXHeadersBuildPhase section */ 123 | 124 | /* Begin PBXNativeTarget section */ 125 | 0FE76AE322FCD15F00F23EE6 /* Palette */ = { 126 | isa = PBXNativeTarget; 127 | buildConfigurationList = 0FE76AEC22FCD15F00F23EE6 /* Build configuration list for PBXNativeTarget "Palette" */; 128 | buildPhases = ( 129 | 0FE76ADF22FCD15F00F23EE6 /* Headers */, 130 | 0FE76AE022FCD15F00F23EE6 /* Sources */, 131 | 0FE76AE122FCD15F00F23EE6 /* Frameworks */, 132 | 0FE76AE222FCD15F00F23EE6 /* Resources */, 133 | ); 134 | buildRules = ( 135 | ); 136 | dependencies = ( 137 | ); 138 | name = Palette; 139 | productName = Palette; 140 | productReference = 0FE76AE422FCD15F00F23EE6 /* Palette.framework */; 141 | productType = "com.apple.product-type.framework"; 142 | }; 143 | /* End PBXNativeTarget section */ 144 | 145 | /* Begin PBXProject section */ 146 | 0FE76ADB22FCD15F00F23EE6 /* Project object */ = { 147 | isa = PBXProject; 148 | attributes = { 149 | LastUpgradeCheck = 1020; 150 | ORGANIZATIONNAME = "Egor Snitsar"; 151 | TargetAttributes = { 152 | 0FE76AE322FCD15F00F23EE6 = { 153 | CreatedOnToolsVersion = 10.2.1; 154 | }; 155 | }; 156 | }; 157 | buildConfigurationList = 0FE76ADE22FCD15F00F23EE6 /* Build configuration list for PBXProject "Palette" */; 158 | compatibilityVersion = "Xcode 9.3"; 159 | developmentRegion = en; 160 | hasScannedForEncodings = 0; 161 | knownRegions = ( 162 | en, 163 | ); 164 | mainGroup = 0FE76ADA22FCD15F00F23EE6; 165 | productRefGroup = 0FE76AE522FCD15F00F23EE6 /* Products */; 166 | projectDirPath = ""; 167 | projectRoot = ""; 168 | targets = ( 169 | 0FE76AE322FCD15F00F23EE6 /* Palette */, 170 | ); 171 | }; 172 | /* End PBXProject section */ 173 | 174 | /* Begin PBXResourcesBuildPhase section */ 175 | 0FE76AE222FCD15F00F23EE6 /* Resources */ = { 176 | isa = PBXResourcesBuildPhase; 177 | buildActionMask = 2147483647; 178 | files = ( 179 | ); 180 | runOnlyForDeploymentPostprocessing = 0; 181 | }; 182 | /* End PBXResourcesBuildPhase section */ 183 | 184 | /* Begin PBXSourcesBuildPhase section */ 185 | 0FE76AE022FCD15F00F23EE6 /* Sources */ = { 186 | isa = PBXSourcesBuildPhase; 187 | buildActionMask = 2147483647; 188 | files = ( 189 | 0F53B15A22FF643700039D92 /* ColorConverter.swift in Sources */, 190 | 0FE76B0222FCD1CA00F23EE6 /* ColorCutQuantizer.swift in Sources */, 191 | 0FE76B0022FCD1CA00F23EE6 /* Target.swift in Sources */, 192 | 0FE76AFD22FCD1CA00F23EE6 /* Heap.swift in Sources */, 193 | 0FE76AFE22FCD1CA00F23EE6 /* CountedSet.swift in Sources */, 194 | 0F53B15822FF63E500039D92 /* TargetBuilder.swift in Sources */, 195 | 0FE76B0122FCD1CA00F23EE6 /* PaletteFilter.swift in Sources */, 196 | 0FE76AFB22FCD1CA00F23EE6 /* Palette.swift in Sources */, 197 | 0FE76AFF22FCD1CA00F23EE6 /* PaletteSwatch.swift in Sources */, 198 | 0FE76AFC22FCD1CA00F23EE6 /* PriorityQueue.swift in Sources */, 199 | 0FE76B0322FCD1CA00F23EE6 /* Color.swift in Sources */, 200 | 0FE76B0622FCD2C100F23EE6 /* UIImage+Palette.swift in Sources */, 201 | 0FE76B0422FCD1CA00F23EE6 /* PaletteBuilder.swift in Sources */, 202 | ); 203 | runOnlyForDeploymentPostprocessing = 0; 204 | }; 205 | /* End PBXSourcesBuildPhase section */ 206 | 207 | /* Begin XCBuildConfiguration section */ 208 | 0FE76AEA22FCD15F00F23EE6 /* Debug */ = { 209 | isa = XCBuildConfiguration; 210 | buildSettings = { 211 | ALWAYS_SEARCH_USER_PATHS = NO; 212 | CLANG_ANALYZER_NONNULL = YES; 213 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 214 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 215 | CLANG_CXX_LIBRARY = "libc++"; 216 | CLANG_ENABLE_MODULES = YES; 217 | CLANG_ENABLE_OBJC_ARC = YES; 218 | CLANG_ENABLE_OBJC_WEAK = YES; 219 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 220 | CLANG_WARN_BOOL_CONVERSION = YES; 221 | CLANG_WARN_COMMA = YES; 222 | CLANG_WARN_CONSTANT_CONVERSION = YES; 223 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 224 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 225 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 226 | CLANG_WARN_EMPTY_BODY = YES; 227 | CLANG_WARN_ENUM_CONVERSION = YES; 228 | CLANG_WARN_INFINITE_RECURSION = YES; 229 | CLANG_WARN_INT_CONVERSION = YES; 230 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 231 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 232 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 233 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 234 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 235 | CLANG_WARN_STRICT_PROTOTYPES = YES; 236 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 237 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 238 | CLANG_WARN_UNREACHABLE_CODE = YES; 239 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 240 | CODE_SIGN_IDENTITY = "iPhone Developer"; 241 | COPY_PHASE_STRIP = NO; 242 | CURRENT_PROJECT_VERSION = 1; 243 | DEBUG_INFORMATION_FORMAT = dwarf; 244 | ENABLE_STRICT_OBJC_MSGSEND = YES; 245 | ENABLE_TESTABILITY = YES; 246 | GCC_C_LANGUAGE_STANDARD = gnu11; 247 | GCC_DYNAMIC_NO_PIC = NO; 248 | GCC_NO_COMMON_BLOCKS = YES; 249 | GCC_OPTIMIZATION_LEVEL = 0; 250 | GCC_PREPROCESSOR_DEFINITIONS = ( 251 | "DEBUG=1", 252 | "$(inherited)", 253 | ); 254 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 255 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 256 | GCC_WARN_UNDECLARED_SELECTOR = YES; 257 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 258 | GCC_WARN_UNUSED_FUNCTION = YES; 259 | GCC_WARN_UNUSED_VARIABLE = YES; 260 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 261 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 262 | MTL_FAST_MATH = YES; 263 | ONLY_ACTIVE_ARCH = YES; 264 | SDKROOT = iphoneos; 265 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 266 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 267 | VERSIONING_SYSTEM = "apple-generic"; 268 | VERSION_INFO_PREFIX = ""; 269 | }; 270 | name = Debug; 271 | }; 272 | 0FE76AEB22FCD15F00F23EE6 /* Release */ = { 273 | isa = XCBuildConfiguration; 274 | buildSettings = { 275 | ALWAYS_SEARCH_USER_PATHS = NO; 276 | CLANG_ANALYZER_NONNULL = YES; 277 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 278 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 279 | CLANG_CXX_LIBRARY = "libc++"; 280 | CLANG_ENABLE_MODULES = YES; 281 | CLANG_ENABLE_OBJC_ARC = YES; 282 | CLANG_ENABLE_OBJC_WEAK = YES; 283 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 284 | CLANG_WARN_BOOL_CONVERSION = YES; 285 | CLANG_WARN_COMMA = YES; 286 | CLANG_WARN_CONSTANT_CONVERSION = YES; 287 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 288 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 289 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 290 | CLANG_WARN_EMPTY_BODY = YES; 291 | CLANG_WARN_ENUM_CONVERSION = YES; 292 | CLANG_WARN_INFINITE_RECURSION = YES; 293 | CLANG_WARN_INT_CONVERSION = YES; 294 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 295 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 296 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 297 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 298 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 299 | CLANG_WARN_STRICT_PROTOTYPES = YES; 300 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 301 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 302 | CLANG_WARN_UNREACHABLE_CODE = YES; 303 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 304 | CODE_SIGN_IDENTITY = "iPhone Developer"; 305 | COPY_PHASE_STRIP = NO; 306 | CURRENT_PROJECT_VERSION = 1; 307 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 308 | ENABLE_NS_ASSERTIONS = NO; 309 | ENABLE_STRICT_OBJC_MSGSEND = YES; 310 | GCC_C_LANGUAGE_STANDARD = gnu11; 311 | GCC_NO_COMMON_BLOCKS = YES; 312 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 313 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 314 | GCC_WARN_UNDECLARED_SELECTOR = YES; 315 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 316 | GCC_WARN_UNUSED_FUNCTION = YES; 317 | GCC_WARN_UNUSED_VARIABLE = YES; 318 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 319 | MTL_ENABLE_DEBUG_INFO = NO; 320 | MTL_FAST_MATH = YES; 321 | SDKROOT = iphoneos; 322 | SWIFT_COMPILATION_MODE = wholemodule; 323 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 324 | VALIDATE_PRODUCT = YES; 325 | VERSIONING_SYSTEM = "apple-generic"; 326 | VERSION_INFO_PREFIX = ""; 327 | }; 328 | name = Release; 329 | }; 330 | 0FE76AED22FCD15F00F23EE6 /* Debug */ = { 331 | isa = XCBuildConfiguration; 332 | buildSettings = { 333 | CODE_SIGN_IDENTITY = ""; 334 | CODE_SIGN_STYLE = Manual; 335 | DEFINES_MODULE = YES; 336 | DEVELOPMENT_TEAM = ""; 337 | DYLIB_COMPATIBILITY_VERSION = 1; 338 | DYLIB_CURRENT_VERSION = 1; 339 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 340 | INFOPLIST_FILE = Palette/Info.plist; 341 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 342 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 343 | LD_RUNPATH_SEARCH_PATHS = ( 344 | "$(inherited)", 345 | "@executable_path/Frameworks", 346 | "@loader_path/Frameworks", 347 | ); 348 | MARKETING_VERSION = 1.0.6; 349 | PRODUCT_BUNDLE_IDENTIFIER = com.snitsar.Palette; 350 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 351 | PROVISIONING_PROFILE_SPECIFIER = ""; 352 | SKIP_INSTALL = YES; 353 | SUPPORTS_MACCATALYST = NO; 354 | SWIFT_VERSION = 5.0; 355 | TARGETED_DEVICE_FAMILY = "1,2"; 356 | }; 357 | name = Debug; 358 | }; 359 | 0FE76AEE22FCD15F00F23EE6 /* Release */ = { 360 | isa = XCBuildConfiguration; 361 | buildSettings = { 362 | CODE_SIGN_IDENTITY = ""; 363 | CODE_SIGN_STYLE = Manual; 364 | DEFINES_MODULE = YES; 365 | DEVELOPMENT_TEAM = ""; 366 | DYLIB_COMPATIBILITY_VERSION = 1; 367 | DYLIB_CURRENT_VERSION = 1; 368 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 369 | INFOPLIST_FILE = Palette/Info.plist; 370 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 371 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 372 | LD_RUNPATH_SEARCH_PATHS = ( 373 | "$(inherited)", 374 | "@executable_path/Frameworks", 375 | "@loader_path/Frameworks", 376 | ); 377 | MARKETING_VERSION = 1.0.6; 378 | PRODUCT_BUNDLE_IDENTIFIER = com.snitsar.Palette; 379 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 380 | PROVISIONING_PROFILE_SPECIFIER = ""; 381 | SKIP_INSTALL = YES; 382 | SUPPORTS_MACCATALYST = NO; 383 | SWIFT_VERSION = 5.0; 384 | TARGETED_DEVICE_FAMILY = "1,2"; 385 | }; 386 | name = Release; 387 | }; 388 | /* End XCBuildConfiguration section */ 389 | 390 | /* Begin XCConfigurationList section */ 391 | 0FE76ADE22FCD15F00F23EE6 /* Build configuration list for PBXProject "Palette" */ = { 392 | isa = XCConfigurationList; 393 | buildConfigurations = ( 394 | 0FE76AEA22FCD15F00F23EE6 /* Debug */, 395 | 0FE76AEB22FCD15F00F23EE6 /* Release */, 396 | ); 397 | defaultConfigurationIsVisible = 0; 398 | defaultConfigurationName = Release; 399 | }; 400 | 0FE76AEC22FCD15F00F23EE6 /* Build configuration list for PBXNativeTarget "Palette" */ = { 401 | isa = XCConfigurationList; 402 | buildConfigurations = ( 403 | 0FE76AED22FCD15F00F23EE6 /* Debug */, 404 | 0FE76AEE22FCD15F00F23EE6 /* Release */, 405 | ); 406 | defaultConfigurationIsVisible = 0; 407 | defaultConfigurationName = Release; 408 | }; 409 | /* End XCConfigurationList section */ 410 | }; 411 | rootObject = 0FE76ADB22FCD15F00F23EE6 /* Project object */; 412 | } 413 | -------------------------------------------------------------------------------- /Framework/Palette.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Framework/Palette.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Framework/Palette.xcodeproj/xcshareddata/xcschemes/Palette.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Framework/Palette.xcodeproj/xcuserdata/galandezzz.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Palette.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 0FE76AE322FCD15F00F23EE6 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Framework/Palette/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Framework/Palette/Palette.h: -------------------------------------------------------------------------------- 1 | // 2 | // Palette.h 3 | // Palette 4 | // 5 | // Created by Egor Snitsar on 09.08.2019. 6 | // Copyright © 2019 Egor Snitsar. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Palette. 12 | FOUNDATION_EXPORT double PaletteVersionNumber; 13 | 14 | //! Project version string for Palette. 15 | FOUNDATION_EXPORT const unsigned char PaletteVersionString[]; 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Egor Snitsar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Palette.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'Palette' 3 | spec.version = '1.0.6' 4 | spec.summary = 'Color palette generation from image written in Swift' 5 | spec.homepage = 'https://github.com/galandezzz/Palette' 6 | spec.license = 'MIT' 7 | spec.author = { 'Egor Snitsar' => 'fearum@icloud.com' } 8 | spec.platform = :ios, '9.0' 9 | spec.swift_version = '5.0' 10 | spec.source = { :git => 'https://github.com/galandezzz/Palette.git', :tag => "v#{spec.version}" } 11 | spec.source_files = 'Source/*', 'Source/*/*' 12 | end 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Palette 2 | 3 | Color palette generation from image written in Swift. 4 | 5 | ## Installation 6 | 7 | **Cocoapods:** 8 | 9 | `pod 'Palette', :git => 'https://github.com/galandezzz/ios-Palette.git'` 10 | 11 | **Carthage:** 12 | 13 | `github "galandezzz/Palette" ~> 1.0` 14 | 15 | ## Usage 16 | 17 | ### Targets 18 | 19 | There are six built-in targets for palette generation: 20 | 21 | - Light vibrant 22 | - Vibrant 23 | - Dark vibrant 24 | - Light muted 25 | - Muted 26 | - Dark muted 27 | 28 | 29 | You can also create your own targets using `Target.Builder` class: 30 | ``` 31 | let target = Target.Builder() 32 | .with(targetSaturation: 0.7) 33 | .with(targetLightness: 0.7) 34 | .build() 35 | ``` 36 | 37 | ### Synchronous Palette generation 38 | 39 | ``` 40 | let palette = Palette.from(image: YOUR_IMAGE).generate() 41 | view.backgroundColor = palette.vibrantColor 42 | ``` 43 | 44 | or simply 45 | 46 | ``` 47 | view.backgroundColor = YOUR_IMAGE.createPalette().vibrantColor 48 | ``` 49 | 50 | ### Asynchornous Palette generation 51 | 52 | ``` 53 | Palette.from(image: YOUR_IMAGE).generate { view.backgroundColor = $0.vibrantColor } 54 | ``` 55 | 56 | or using extension on `UIImage` 57 | 58 | ``` 59 | YOUR_IMAGE.createPalette { view.backgroudColor = $0.vibrantColor } 60 | ``` 61 | 62 | ## License 63 | 64 | Palette is available under the MIT license. See the LICENSE file for more info. 65 | -------------------------------------------------------------------------------- /Source/Color.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color.swift 3 | // Palette 4 | // 5 | // Created by Egor Snitsar on 08.08.2019. 6 | // Copyright © 2019 Egor Snitsar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | 13 | internal convenience init(_ color: Color) { 14 | self.init(red: CGFloat(color.red) / 255.0, 15 | green: CGFloat(color.green) / 255.0, 16 | blue: CGFloat(color.blue) / 255.0, 17 | alpha: 1.0) 18 | } 19 | } 20 | 21 | internal struct Color: Hashable, Comparable, CustomDebugStringConvertible { 22 | 23 | internal enum Width: Int { 24 | case normal = 8 25 | case quantized = 5 26 | } 27 | 28 | internal init(_ storage: Int, width: Width = .normal) { 29 | self.storage = storage 30 | self.width = width 31 | } 32 | 33 | internal init(_ components: [Int], width: Width = .normal) { 34 | self.storage = ColorConverter.packColor(components: components, width: width.rawValue) 35 | self.width = width 36 | } 37 | 38 | internal init(reducingAlpha components: [Int], width: Width = .normal) { 39 | let alpha = components[3] 40 | let cs = components[0...2].map { ColorConverter.reduceAlpha(for: $0, alpha: alpha) } 41 | self.init(cs, width: width) 42 | } 43 | 44 | internal init(_ components: [UInt8], width: Width = .normal) { 45 | self.init(components.map { Int($0) }, width: width) 46 | } 47 | 48 | internal init(reducingAlpha components: [UInt8], width: Width = .normal) { 49 | self.init(reducingAlpha: components.map { Int($0) }, width: width) 50 | } 51 | 52 | internal var red: Int { 53 | return (storage >> (width.rawValue * 2)) & mask 54 | } 55 | 56 | internal var green: Int { 57 | return (storage >> width.rawValue) & mask 58 | } 59 | 60 | internal var blue: Int { 61 | return storage & mask 62 | } 63 | 64 | internal var hsl: HSL { 65 | return ColorConverter.colorToHSL(self) 66 | } 67 | 68 | internal var rgb: RGB { 69 | return (red, green, blue) 70 | } 71 | 72 | internal var quantized: Color { 73 | return color(with: .quantized) 74 | } 75 | 76 | internal var normalized: Color { 77 | return color(with: .normal) 78 | } 79 | 80 | internal let width: Width 81 | 82 | // MARK: - CustomDebugStringConvertible 83 | 84 | var debugDescription: String { 85 | return """ 86 | 87 | Red: \(red), Green: \(green), Blue: \(blue) 88 | Hue: \(hsl.h), Saturation: \(hsl.s), Brightness: \(hsl.l) 89 | """ 90 | } 91 | 92 | // MARK: - Comparable 93 | 94 | internal static func < (lhs: Color, rhs: Color) -> Bool { 95 | return lhs.storage < rhs.storage 96 | } 97 | 98 | // MARK: - Private 99 | 100 | private let storage: Int 101 | 102 | private var mask: Int { 103 | return (1 << width.rawValue) - 1 104 | } 105 | 106 | private func color(with width: Width) -> Color { 107 | let r = ColorConverter.modifyWordWidth(red, currentWidth: self.width.rawValue, targetWidth: width.rawValue) 108 | let g = ColorConverter.modifyWordWidth(green, currentWidth: self.width.rawValue, targetWidth: width.rawValue) 109 | let b = ColorConverter.modifyWordWidth(blue, currentWidth: self.width.rawValue, targetWidth: width.rawValue) 110 | 111 | return Color([r, g, b], width: width) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Source/ColorConverter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorUtils.swift 3 | // Palette 4 | // 5 | // Created by Egor Snitsar on 10.08.2019. 6 | // Copyright © 2019 Egor Snitsar. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | internal struct ColorConverter { 12 | 13 | internal static func colorToHSL(_ color: Color) -> HSL { 14 | let r = CGFloat(color.red) / 255.0 15 | let g = CGFloat(color.green) / 255.0 16 | let b = CGFloat(color.blue) / 255.0 17 | 18 | let cmin = min(r, g, b) 19 | let cmax = max(r, g, b) 20 | let delta = cmax - cmin 21 | 22 | var h: CGFloat = 0.0 23 | var s: CGFloat = 0.0 24 | let l = (cmax + cmin) / 2.0 25 | 26 | if cmax != cmin { 27 | switch cmax { 28 | case r: 29 | h = ((g - b) / delta).truncatingRemainder(dividingBy: 6.0) 30 | case g: 31 | h = ((b - r) / delta) + 2.0 32 | default: 33 | h = ((r - g) / delta) + 4.0 34 | } 35 | 36 | s = delta / (1 - abs(2 * l - 1)) 37 | } 38 | 39 | h = (h * 60.0).truncatingRemainder(dividingBy: 360.0) 40 | if h.isLess(than: .zero) { 41 | h += 360.0 42 | } 43 | 44 | return ( 45 | h.rounded().limited(.zero, 360.0), 46 | s.limited(.zero, 1.0), 47 | l.limited(.zero, 1.0) 48 | ) 49 | } 50 | 51 | internal static func reduceAlpha(for value: Int, alpha: Int) -> Int { 52 | guard alpha > .zero else { 53 | return value 54 | } 55 | 56 | return Int(CGFloat(value) / CGFloat(alpha) * 255.0) 57 | } 58 | 59 | internal static func packColor(components: [Int], width: Int) -> Int { 60 | let mask: Int = (1 << width) - 1 61 | 62 | let r = components[0] 63 | let g = components[1] 64 | let b = components[2] 65 | 66 | return ((r & mask) << (width * 2)) | ((g & mask) << width) | (b & mask) 67 | } 68 | 69 | internal static func packColor(components: [UInt8], width: Int) -> Int { 70 | return packColor(components: components.map { Int($0) }, width: width) 71 | } 72 | 73 | internal static func modifyWordWidth(_ value: Int, currentWidth: Int, targetWidth: Int) -> Int { 74 | guard currentWidth != targetWidth else { 75 | return value 76 | } 77 | 78 | let newValue: Int 79 | if targetWidth > currentWidth { 80 | newValue = value << (targetWidth - currentWidth) 81 | } else { 82 | newValue = value >> (currentWidth - targetWidth) 83 | } 84 | 85 | return newValue & ((1 << targetWidth) - 1) 86 | } 87 | } 88 | 89 | private extension Comparable { 90 | 91 | func limited(_ lowerBound: Self, _ upperBound: Self) -> Self { 92 | return min(max(lowerBound, self), upperBound) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Source/ColorCutQuantizer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorCutQuantizer.swift 3 | // Palette 4 | // 5 | // Created by Egor Snitsar on 06.08.2019. 6 | // Copyright © 2019 Egor Snitsar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal final class ColorCutQuantizer { 12 | 13 | internal var quantizedColors = [Palette.Swatch]() 14 | 15 | internal init(colors: [Color], maxColorsCount: Int, filters: [PaletteFilter]) { 16 | self.filters = filters 17 | 18 | let hist = CountedSet( 19 | colors 20 | .map { $0.quantized } 21 | .filter { !shouldIgnoreColor($0.normalized) } 22 | ) 23 | 24 | var distinctColors = hist.allObjects 25 | 26 | if distinctColors.count <= maxColorsCount { 27 | quantizedColors = distinctColors.map { Palette.Swatch(color: $0.normalized, population: hist.count(for: $0)) } 28 | } else { 29 | quantizedColors = quantizePixels(maxColorsCount: maxColorsCount, colors: &distinctColors, histogram: hist) 30 | } 31 | } 32 | 33 | private let filters: [PaletteFilter] 34 | 35 | private func shouldIgnoreColor(_ swatch: Palette.Swatch) -> Bool { 36 | return shouldIgnoreColor(swatch._color) 37 | } 38 | 39 | private func shouldIgnoreColor(_ color: Color) -> Bool { 40 | return filters.contains { !$0.isAllowed(rgb: color.rgb, hsl: color.hsl) } 41 | } 42 | 43 | private func quantizePixels(maxColorsCount: Int, colors: inout [Color], histogram: CountedSet) -> [Palette.Swatch] { 44 | var queue = PriorityQueue() { $0.volume > $1.volume } 45 | queue.enqueue(VBox(lowerIndex: colors.startIndex, upperIndex: colors.index(before: colors.endIndex), colors: colors, histogram: histogram)) 46 | splitBoxes(queue: &queue, maxSize: maxColorsCount, colors: &colors, histogram: histogram) 47 | 48 | return generateAverageColors(from: queue.elements, colors: colors, histogram: histogram) 49 | } 50 | 51 | private func splitBoxes(queue: inout PriorityQueue, maxSize: Int, colors: inout [Color], histogram: CountedSet) { 52 | while queue.count < maxSize { 53 | if let vbox = queue.dequeue(), vbox.canSplit { 54 | if let newBox = vbox.splitBox(colors: &colors, histogram: histogram) { 55 | queue.enqueue(newBox) 56 | } 57 | queue.enqueue(vbox) 58 | } else { 59 | return 60 | } 61 | } 62 | } 63 | 64 | private func generateAverageColors(from boxes: [VBox], colors: [Color], histogram: CountedSet) -> [Palette.Swatch] { 65 | return boxes.compactMap { 66 | let swatch = $0.averageColor(colors: colors, histogram: histogram) 67 | 68 | guard !shouldIgnoreColor(swatch) else { 69 | return nil 70 | } 71 | 72 | return swatch 73 | } 74 | } 75 | 76 | private class VBox { 77 | 78 | internal init(lowerIndex: Int, upperIndex: Int, colors: [Color], histogram: CountedSet) { 79 | self.lowerIndex = lowerIndex 80 | self.upperIndex = upperIndex 81 | fitBox(colors: colors, histogram: histogram) 82 | } 83 | 84 | internal var volume: Int { 85 | return (maxRed - minRed + 1) * (maxGreen - minGreen + 1) * (maxBlue - minBlue + 1) 86 | } 87 | 88 | internal var canSplit: Bool { 89 | return colorCount > 1 90 | } 91 | 92 | internal func splitBox(colors: inout [Color], histogram: CountedSet) -> VBox? { 93 | guard canSplit else { 94 | return nil 95 | } 96 | 97 | let splitPoint = findSplitPoint(colors: &colors, histogram: histogram) 98 | let newBox = VBox(lowerIndex: splitPoint + 1, upperIndex: upperIndex, colors: colors, histogram: histogram) 99 | 100 | upperIndex = splitPoint 101 | fitBox(colors: colors, histogram: histogram) 102 | 103 | return newBox 104 | } 105 | 106 | internal func averageColor(colors: [Color], histogram: CountedSet) -> Palette.Swatch { 107 | var redSum = 0, greenSum = 0, blueSum = 0, totalCount = 0 108 | 109 | colors[lowerIndex...upperIndex].forEach { 110 | let (r, g, b) = $0.rgb 111 | let count = histogram.count(for: $0) 112 | 113 | totalCount += count 114 | redSum += count * Int(r) 115 | greenSum += count * Int(g) 116 | blueSum += count * Int(b) 117 | } 118 | 119 | let mean: (Int) -> Int = { Int((CGFloat($0) / CGFloat(totalCount)).rounded()) } 120 | 121 | let redMean = mean(redSum) 122 | let greenMean = mean(greenSum) 123 | let blueMean = mean(blueSum) 124 | 125 | let color = Color([redMean, greenMean, blueMean], width: .quantized) 126 | 127 | return Palette.Swatch(color: color.normalized, population: totalCount) 128 | } 129 | 130 | // MARK: - Private 131 | 132 | private enum Component { 133 | case red 134 | case green 135 | case blue 136 | } 137 | 138 | private let lowerIndex: Int 139 | private var upperIndex: Int 140 | 141 | private var population = 0 142 | 143 | private var minRed = 0, maxRed = 0 144 | private var minGreen = 0, maxGreen = 0 145 | private var minBlue = 0, maxBlue = 0 146 | 147 | private var colorCount: Int { 148 | return upperIndex - lowerIndex + 1 149 | } 150 | 151 | private func fitBox(colors: [Color], histogram: CountedSet) { 152 | minRed = Int.max 153 | minGreen = Int.max 154 | minBlue = Int.max 155 | maxRed = Int.min 156 | maxGreen = Int.min 157 | maxBlue = Int.min 158 | 159 | for i in (lowerIndex...upperIndex) { 160 | let color = colors[i] 161 | population += histogram.count(for: color) 162 | 163 | let r = Int(color.red) 164 | let g = Int(color.green) 165 | let b = Int(color.blue) 166 | 167 | if r > maxRed { maxRed = r } 168 | if r < minRed { minRed = r } 169 | 170 | if g > maxGreen { maxGreen = g } 171 | if g < minGreen { minGreen = g } 172 | 173 | if b > maxBlue { maxBlue = b } 174 | if b < minBlue { minBlue = b } 175 | } 176 | } 177 | 178 | private func findLongestComponent() -> Component { 179 | let redLength = maxRed - minRed 180 | let greenLength = maxGreen - minGreen 181 | let blueLength = maxBlue - minBlue 182 | 183 | if redLength >= greenLength && redLength >= blueLength { 184 | return .red 185 | } else if greenLength >= redLength && greenLength >= blueLength { 186 | return .green 187 | } else { 188 | return .blue 189 | } 190 | } 191 | 192 | private func findSplitPoint(colors: inout [Color], histogram: CountedSet) -> Int { 193 | let longestComponent = findLongestComponent() 194 | 195 | modifySignificantOctet(for: &colors, component: longestComponent, lower: lowerIndex, upper: upperIndex) 196 | 197 | colors[lowerIndex...upperIndex].sort() 198 | 199 | modifySignificantOctet(for: &colors, component: longestComponent, lower: lowerIndex, upper: upperIndex) 200 | 201 | let midPoint = population / 2 202 | var count = 0 203 | 204 | for i in (lowerIndex...upperIndex) { 205 | count += histogram.count(for: colors[i]) 206 | 207 | if count >= midPoint { 208 | return min(upperIndex - 1, i) 209 | } 210 | } 211 | 212 | return lowerIndex 213 | } 214 | 215 | private func modifySignificantOctet(for colors: inout [Color], component: Component, lower: Int, upper: Int) { 216 | switch component { 217 | case .red: 218 | break 219 | 220 | case .green: 221 | for i in (lower...upper) { 222 | let (r, g, b) = colors[i].rgb 223 | colors[i] = Color([g, r, b], width: colors[i].width) 224 | } 225 | 226 | case .blue: 227 | for i in (lower...upper) { 228 | let (r, g, b) = colors[i].rgb 229 | colors[i] = Color([b, g, r], width: colors[i].width) 230 | } 231 | } 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /Source/Data Structures/CountedSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountedSet.swift 3 | // Palette 4 | // 5 | // Created by Egor Snitsar on 08.08.2019. 6 | // Copyright © 2019 Egor Snitsar. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | internal struct CountedSet { 12 | 13 | internal init(_ array: [T] = []) { 14 | self.storage = NSCountedSet(array: array) 15 | } 16 | 17 | internal var allObjects: [T] { 18 | return storage.allObjects as! [T] 19 | } 20 | 21 | internal var countedObjects: [T: Int] { 22 | let values = allObjects.map { ($0, count(for: $0)) } 23 | 24 | return Dictionary(uniqueKeysWithValues: values) 25 | } 26 | 27 | internal func contains(_ object: T) -> Bool { 28 | return storage.contains(object) 29 | } 30 | 31 | internal func insert(_ object: T) { 32 | storage.add(object) 33 | } 34 | 35 | internal func remove(_ object: T) { 36 | storage.remove(object) 37 | } 38 | 39 | internal func count(for object: T) -> Int { 40 | return storage.count(for: object) 41 | } 42 | 43 | private let storage: NSCountedSet 44 | } 45 | -------------------------------------------------------------------------------- /Source/Data Structures/Heap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Heap.swift 3 | // Palette 4 | // 5 | // Created by Egor Snitsar on 07.08.2019. 6 | // Copyright © 2019 Egor Snitsar. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | internal struct Heap { 12 | 13 | /** 14 | * Creates an empty heap. 15 | * The sort function determines whether this is a min-heap or max-heap. 16 | * For comparable data types, > makes a max-heap, < makes a min-heap. 17 | */ 18 | internal init(sort: @escaping (T, T) -> Bool) { 19 | self.orderCriteria = sort 20 | } 21 | 22 | /** 23 | * Creates a heap from an array. The order of the array does not matter; 24 | * the elements are inserted into the heap in the order determined by the 25 | * sort function. For comparable data types, '>' makes a max-heap, 26 | * '<' makes a min-heap. 27 | */ 28 | internal init(array: [T], sort: @escaping (T, T) -> Bool) { 29 | self.orderCriteria = sort 30 | configureHeap(from: array) 31 | } 32 | 33 | /** 34 | * Configures the max-heap or min-heap from an array, in a bottom-up manner. 35 | * Performance: This runs pretty much in O(n). 36 | */ 37 | internal mutating func configureHeap(from array: [T]) { 38 | nodes = array 39 | for i in stride(from: (nodes.count / 2 - 1), through: 0, by: -1) { 40 | shiftDown(i) 41 | } 42 | } 43 | 44 | /** The array that stores the heap's nodes. */ 45 | internal var nodes = [T]() 46 | 47 | internal var isEmpty: Bool { 48 | return nodes.isEmpty 49 | } 50 | 51 | internal var count: Int { 52 | return nodes.count 53 | } 54 | 55 | /** 56 | * Returns the index of the parent of the element at index i. 57 | * The element at index 0 is the root of the tree and has no parent. 58 | */ 59 | @inline(__always) internal func parentIndex(ofIndex i: Int) -> Int { 60 | return (i - 1) / 2 61 | } 62 | 63 | /** 64 | * Returns the index of the left child of the element at index i. 65 | * Note that this index can be greater than the heap size, in which case 66 | * there is no left child. 67 | */ 68 | @inline(__always) internal func leftChildIndex(ofIndex i: Int) -> Int { 69 | return 2*i + 1 70 | } 71 | 72 | /** 73 | * Returns the index of the right child of the element at index i. 74 | * Note that this index can be greater than the heap size, in which case 75 | * there is no right child. 76 | */ 77 | @inline(__always) internal func rightChildIndex(ofIndex i: Int) -> Int { 78 | return 2*i + 2 79 | } 80 | 81 | /** 82 | * Returns the maximum value in the heap (for a max-heap) or the minimum 83 | * value (for a min-heap). 84 | */ 85 | internal func peek() -> T? { 86 | return nodes.first 87 | } 88 | 89 | /** 90 | * Adds a new value to the heap. This reorders the heap so that the max-heap 91 | * or min-heap property still holds. Performance: O(log n). 92 | */ 93 | internal mutating func insert(_ value: T) { 94 | nodes.append(value) 95 | shiftUp(nodes.count - 1) 96 | } 97 | 98 | /** 99 | * Adds a sequence of values to the heap. This reorders the heap so that 100 | * the max-heap or min-heap property still holds. Performance: O(log n). 101 | */ 102 | internal mutating func insert(_ sequence: S) where S.Iterator.Element == T { 103 | for value in sequence { 104 | insert(value) 105 | } 106 | } 107 | 108 | /** 109 | * Allows you to change an element. This reorders the heap so that 110 | * the max-heap or min-heap property still holds. 111 | */ 112 | internal mutating func replace(index i: Int, value: T) { 113 | guard i < nodes.count else { 114 | return 115 | } 116 | 117 | remove(at: i) 118 | insert(value) 119 | } 120 | 121 | /** 122 | * Removes the root node from the heap. For a max-heap, this is the maximum 123 | * value; for a min-heap it is the minimum value. Performance: O(log n). 124 | */ 125 | @discardableResult internal mutating func remove() -> T? { 126 | guard !nodes.isEmpty else { 127 | return nil 128 | } 129 | 130 | if nodes.count == 1 { 131 | return nodes.removeLast() 132 | } else { 133 | // Use the last node to replace the first one, then fix the heap by 134 | // shifting this new first node into its proper position. 135 | let value = nodes[0] 136 | nodes[0] = nodes.removeLast() 137 | shiftDown(0) 138 | 139 | return value 140 | } 141 | } 142 | 143 | /** 144 | * Removes an arbitrary node from the heap. Performance: O(log n). 145 | * Note that you need to know the node's index. 146 | */ 147 | @discardableResult internal mutating func remove(at index: Int) -> T? { 148 | guard index < nodes.count else { 149 | return nil 150 | } 151 | 152 | let size = nodes.count - 1 153 | if index != size { 154 | nodes.swapAt(index, size) 155 | shiftDown(from: index, until: size) 156 | shiftUp(index) 157 | } 158 | 159 | return nodes.removeLast() 160 | } 161 | 162 | /** 163 | * Takes a child node and looks at its parents; if a parent is not larger 164 | * (max-heap) or not smaller (min-heap) than the child, we exchange them. 165 | */ 166 | internal mutating func shiftUp(_ index: Int) { 167 | var childIndex = index 168 | let child = nodes[childIndex] 169 | var parentIndex = self.parentIndex(ofIndex: childIndex) 170 | 171 | while childIndex > 0 && orderCriteria(child, nodes[parentIndex]) { 172 | nodes[childIndex] = nodes[parentIndex] 173 | childIndex = parentIndex 174 | parentIndex = self.parentIndex(ofIndex: childIndex) 175 | } 176 | 177 | nodes[childIndex] = child 178 | } 179 | 180 | /** 181 | * Looks at a parent node and makes sure it is still larger (max-heap) or 182 | * smaller (min-heap) than its childeren. 183 | */ 184 | internal mutating func shiftDown(from index: Int, until endIndex: Int) { 185 | let leftChildIndex = self.leftChildIndex(ofIndex: index) 186 | let rightChildIndex = leftChildIndex + 1 187 | 188 | // Figure out which comes first if we order them by the sort function: 189 | // the parent, the left child, or the right child. If the parent comes 190 | // first, we're done. If not, that element is out-of-place and we make 191 | // it "float down" the tree until the heap property is restored. 192 | var first = index 193 | if leftChildIndex < endIndex && orderCriteria(nodes[leftChildIndex], nodes[first]) { 194 | first = leftChildIndex 195 | } 196 | if rightChildIndex < endIndex && orderCriteria(nodes[rightChildIndex], nodes[first]) { 197 | first = rightChildIndex 198 | } 199 | if first == index { return } 200 | 201 | nodes.swapAt(index, first) 202 | shiftDown(from: first, until: endIndex) 203 | } 204 | 205 | internal mutating func shiftDown(_ index: Int) { 206 | shiftDown(from: index, until: nodes.count) 207 | } 208 | 209 | /** 210 | * Determines how to compare two nodes in the heap. 211 | * Use '>' for a max-heap or '<' for a min-heap, 212 | * or provide a comparing method if the heap is made 213 | * of custom elements, for example tuples. 214 | */ 215 | private var orderCriteria: (T, T) -> Bool 216 | 217 | } 218 | 219 | // MARK: - Searching 220 | 221 | extension Heap where T: Equatable { 222 | 223 | /** Get the index of a node in the heap. Performance: O(n). */ 224 | internal func index(of node: T) -> Int? { 225 | return nodes.firstIndex { $0 == node } 226 | } 227 | 228 | /** Removes the first occurrence of a node from the heap. Performance: O(n log n). */ 229 | @discardableResult internal mutating func remove(node: T) -> T? { 230 | guard let index = index(of: node) else { 231 | return nil 232 | } 233 | 234 | return remove(at: index) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /Source/Data Structures/PriorityQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PriorityQueue.swift 3 | // Palette 4 | // 5 | // Created by Egor Snitsar on 07.08.2019. 6 | // Copyright © 2019 Egor Snitsar. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | internal struct PriorityQueue { 12 | 13 | private var heap: Heap 14 | 15 | internal var elements: [T] { 16 | return heap.nodes 17 | } 18 | 19 | /* 20 | To create a max-priority queue, supply a > sort function. For a min-priority 21 | queue, use <. 22 | */ 23 | internal init(sort: @escaping (T, T) -> Bool) { 24 | heap = Heap(sort: sort) 25 | } 26 | 27 | internal var isEmpty: Bool { 28 | return heap.isEmpty 29 | } 30 | 31 | internal var count: Int { 32 | return heap.count 33 | } 34 | 35 | internal func peek() -> T? { 36 | return heap.peek() 37 | } 38 | 39 | internal mutating func enqueue(_ element: T) { 40 | heap.insert(element) 41 | } 42 | 43 | internal mutating func dequeue() -> T? { 44 | return heap.remove() 45 | } 46 | 47 | /* 48 | Allows you to change the priority of an element. In a max-priority queue, 49 | the new priority should be larger than the old one; in a min-priority queue 50 | it should be smaller. 51 | */ 52 | internal mutating func changePriority(index i: Int, value: T) { 53 | return heap.replace(index: i, value: value) 54 | } 55 | } 56 | 57 | extension PriorityQueue where T: Equatable { 58 | 59 | internal func index(of element: T) -> Int? { 60 | return heap.index(of: element) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Source/Palette.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Palette.swift 3 | // Palette 4 | // 5 | // Created by Egor Snitsar on 05.08.2019. 6 | // Copyright © 2019 Egor Snitsar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public final class Palette { 12 | 13 | // MARK: - Public 14 | 15 | public let swatches: [Swatch] 16 | 17 | public class func from(image: UIImage) -> Builder { 18 | return Builder(image: image) 19 | } 20 | 21 | public var lightVibrantSwatch: Swatch? { 22 | return swatch(for: .lightVibrant) 23 | } 24 | 25 | public var lightVibrantColor: UIColor? { 26 | return lightVibrantSwatch?.color 27 | } 28 | 29 | public var vibrantSwatch: Swatch? { 30 | return swatch(for: .vibrant) 31 | } 32 | 33 | public var vibrantColor: UIColor? { 34 | return vibrantSwatch?.color 35 | } 36 | 37 | public var darkVibrantSwatch: Swatch? { 38 | return swatch(for: .darkVibrant) 39 | } 40 | 41 | public var darkVibrantColor: UIColor? { 42 | return darkVibrantSwatch?.color 43 | } 44 | 45 | public var lightMutedSwatch: Swatch? { 46 | return swatch(for: .lightMuted) 47 | } 48 | 49 | public var lightMutedColor: UIColor? { 50 | return lightMutedSwatch?.color 51 | } 52 | 53 | public var mutedSwatch: Swatch? { 54 | return swatch(for: .muted) 55 | } 56 | 57 | public var mutedColor: UIColor? { 58 | return mutedSwatch?.color 59 | } 60 | 61 | public var darkMutedSwatch: Swatch? { 62 | return swatch(for: .darkMuted) 63 | } 64 | 65 | public var darkMutedColor:UIColor? { 66 | return darkMutedSwatch?.color 67 | } 68 | 69 | public private(set) lazy var dominantSwatch: Swatch? = { 70 | return swatches.max { $0.population < $1.population } 71 | }() 72 | 73 | public var dominantColor: UIColor? { 74 | return dominantSwatch?.color 75 | } 76 | 77 | public func swatch(for target: Target) -> Swatch? { 78 | return selectedSwatches[target] 79 | } 80 | 81 | public func color(for target: Target) -> UIColor? { 82 | return swatch(for: target)?.color 83 | } 84 | 85 | // MARK: - Internal 86 | 87 | internal init(swatches: [Swatch], targets: [Target]) { 88 | self.swatches = swatches 89 | self.targets = targets 90 | } 91 | 92 | internal func generate() { 93 | targets.forEach { 94 | $0.normalizeWeights() 95 | selectedSwatches[$0] = scoredSwatch(for: $0) 96 | } 97 | 98 | usedColors.removeAll() 99 | } 100 | 101 | // MARK: - Private 102 | 103 | private let targets: [Target] 104 | 105 | private var selectedSwatches = [Target: Swatch]() 106 | private var usedColors = Set() 107 | 108 | private func scoredSwatch(for target: Target) -> Swatch? { 109 | guard let swatch = maxScoredSwatch(for: target) else { 110 | return nil 111 | } 112 | 113 | if target.isExclusive { 114 | usedColors.insert(swatch._color) 115 | } 116 | 117 | return swatch 118 | } 119 | 120 | private func maxScoredSwatch(for target: Target) -> Swatch? { 121 | let result = swatches 122 | .filter { shouldBeScored($0, for: target) } 123 | .map { (swatch: $0, score: score($0, target: target)) } 124 | .max { $0.score < $1.score } 125 | 126 | return result?.swatch 127 | } 128 | 129 | private func shouldBeScored(_ swatch: Swatch, for target: Target) -> Bool { 130 | let hsl = swatch.hsl 131 | 132 | return (target.minimumSaturation...target.maximumSaturation).contains(hsl.s) 133 | && (target.minimumLightness...target.maximumLightness).contains(hsl.l) 134 | && !usedColors.contains(swatch._color) 135 | } 136 | 137 | private func score(_ swatch: Swatch, target: Target) -> CGFloat { 138 | let hsl = swatch.hsl 139 | let maxPopulation = CGFloat(dominantSwatch?.population ?? 1) 140 | 141 | let saturationScore = target.saturationWeight * (1 - abs(hsl.s - target.targetSaturation)) 142 | let lightnessScore = target.lightnessWeight * (1 - abs(hsl.l - target.targetLightness)) 143 | let populationScore = target.populationWeight * CGFloat(swatch.population) / maxPopulation 144 | 145 | return saturationScore + lightnessScore + populationScore 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Source/PaletteBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PaletteBuilder.swift 3 | // Palette 4 | // 5 | // Created by Egor Snitsar on 05.08.2019. 6 | // Copyright © 2019 Egor Snitsar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension Palette { 12 | 13 | public final class Builder { 14 | 15 | // MARK: - Public 16 | 17 | public func with(maximumColorsCount: Int) -> Builder { 18 | self.maxColorsCount = maximumColorsCount 19 | 20 | return self 21 | } 22 | 23 | public func with(resizeArea: CGFloat) -> Builder { 24 | self.resizeArea = resizeArea 25 | 26 | return self 27 | } 28 | 29 | public func byRemovingFilters() -> Builder { 30 | self.filters.removeAll() 31 | 32 | return self 33 | } 34 | 35 | public func byAddingFilter(_ filter: PaletteFilter) -> Builder { 36 | self.filters.append(filter) 37 | 38 | return self 39 | } 40 | 41 | public func byRemovingTargets() -> Builder { 42 | self.targets.removeAll() 43 | 44 | return self 45 | } 46 | 47 | public func byAddingTarget(_ target: Target) -> Builder { 48 | self.targets.append(target) 49 | 50 | return self 51 | } 52 | 53 | public func generate() -> Palette { 54 | let swatches: [Swatch] 55 | 56 | if let image = image { 57 | let scaledImage = scaleDownImage(image, to: resizeArea) 58 | let colors = calculateColors(from: scaledImage) 59 | let quantizer = ColorCutQuantizer(colors: colors, maxColorsCount: maxColorsCount, filters: filters) 60 | swatches = quantizer.quantizedColors 61 | } else { 62 | swatches = self.swatches 63 | } 64 | 65 | let p = Palette(swatches: swatches, targets: targets) 66 | p.generate() 67 | 68 | return p 69 | } 70 | 71 | public func generate(_ completion: @escaping (Palette) -> Void) { 72 | DispatchQueue.global(qos: .userInitiated).async { 73 | let palette = self.generate() 74 | 75 | DispatchQueue.main.async { 76 | completion(palette) 77 | } 78 | } 79 | } 80 | 81 | // MARK: - Internal 82 | 83 | internal init(image: UIImage) { 84 | self.image = image 85 | 86 | self.filters.append(DefaultFilter()) 87 | 88 | self.targets.append(.lightVibrant) 89 | self.targets.append(.vibrant) 90 | self.targets.append(.darkVibrant) 91 | self.targets.append(.lightMuted) 92 | self.targets.append(.muted) 93 | self.targets.append(.darkMuted) 94 | } 95 | 96 | internal init(swatches: [Swatch]) { 97 | self.image = nil 98 | self.filters.append(DefaultFilter()) 99 | self.swatches = swatches 100 | } 101 | 102 | // MARK: - Private 103 | 104 | private struct Constants { 105 | static let defaultMaxColorsCount = 16 106 | static let defaultResizeBitmapArea: CGFloat = 112.0 * 112.0 107 | } 108 | 109 | private var maxColorsCount = Constants.defaultMaxColorsCount 110 | private var resizeArea = Constants.defaultResizeBitmapArea 111 | 112 | private let image: UIImage? 113 | private var swatches = [Swatch]() 114 | private var targets = [Target]() 115 | private var filters = [PaletteFilter]() 116 | 117 | private func scaleDownImage(_ image: UIImage, to resizeArea: CGFloat) -> UIImage { 118 | let bitmapArea = image.size.width * image.size.height 119 | 120 | guard bitmapArea > resizeArea else { 121 | return image 122 | } 123 | 124 | let ratio = sqrt(resizeArea / bitmapArea) 125 | let width = ceil(ratio * image.size.width) 126 | let height = ceil(ratio * image.size.height) 127 | let size = CGSize(width: width, height: height) 128 | 129 | UIGraphicsBeginImageContext(size) 130 | 131 | image.draw(in: CGRect(origin: .zero, size: size)) 132 | let resultImage = UIGraphicsGetImageFromCurrentImageContext() 133 | 134 | UIGraphicsEndImageContext() 135 | 136 | return resultImage ?? image 137 | } 138 | 139 | private func calculateColors(from image: UIImage) -> [Color] { 140 | guard let cgImage = image.cgImage else { 141 | return [] 142 | } 143 | 144 | let width = cgImage.width 145 | let height = cgImage.height 146 | 147 | let bytesPerRow = width * 4 148 | let bytesCount = bytesPerRow * height 149 | 150 | let colorSpace = CGColorSpaceCreateDeviceRGB() 151 | 152 | var data = Array(repeating: UInt8(0), count: bytesCount) 153 | 154 | let context = CGContext(data: &data, 155 | width: width, 156 | height: height, 157 | bitsPerComponent: 8, 158 | bytesPerRow: bytesPerRow, 159 | space: colorSpace, 160 | bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) 161 | 162 | let size = CGSize(width: width, height: height) 163 | let rect = CGRect(origin: .zero, size: size) 164 | 165 | context?.draw(cgImage, in: rect) 166 | 167 | return data.chunk(into: 4).map { Color(reducingAlpha: $0) } 168 | } 169 | } 170 | } 171 | 172 | private extension Collection where Index: Strideable { 173 | 174 | func chunk(into size: Index.Stride) -> [[Element]] { 175 | return stride(from: startIndex, to: endIndex, by: size).map { 176 | Array(self[$0 ..< Swift.min($0.advanced(by: size), endIndex)]) 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Source/PaletteFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PaletteFilter.swift 3 | // Palette 4 | // 5 | // Created by Egor Snitsar on 06.08.2019. 6 | // Copyright © 2019 Egor Snitsar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol PaletteFilter { 12 | func isAllowed(rgb: RGB, hsl: HSL) -> Bool 13 | } 14 | 15 | internal struct DefaultFilter: PaletteFilter { 16 | 17 | private struct Constants { 18 | static let blackMaxLightness: CGFloat = 0.05 19 | static let whiteMinLightness: CGFloat = 0.95 20 | static let iLineHueRange: ClosedRange = (10...37) 21 | static let iLineSaturationRange: ClosedRange = (0...0.82) 22 | } 23 | 24 | func isAllowed(rgb: RGB, hsl: HSL) -> Bool { 25 | return !isWhite(hsl) && !isBlack(hsl) && !isNearRedILine(hsl) 26 | } 27 | 28 | private func isBlack(_ hsl: HSL) -> Bool { 29 | return hsl.l <= Constants.blackMaxLightness 30 | } 31 | 32 | private func isWhite(_ hsl: HSL) -> Bool { 33 | return hsl.l >= Constants.whiteMinLightness 34 | } 35 | 36 | private func isNearRedILine(_ hsl: HSL) -> Bool { 37 | return Constants.iLineHueRange.contains(hsl.h) && Constants.iLineSaturationRange.contains(hsl.s) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Source/PaletteSwatch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PaletteSwatch.swift 3 | // Palette 4 | // 5 | // Created by Egor Snitsar on 06.08.2019. 6 | // Copyright © 2019 Egor Snitsar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public typealias RGB = (r: Int, g: Int, b: Int) 12 | public typealias HSL = (h: CGFloat, s: CGFloat, l: CGFloat) 13 | 14 | extension Palette { 15 | 16 | public final class Swatch: CustomDebugStringConvertible { 17 | 18 | public private(set) lazy var color = UIColor(_color) 19 | 20 | public private(set) lazy var hsl: HSL = _color.hsl 21 | public private(set) lazy var rgb: RGB = _color.rgb 22 | 23 | public let population: Int 24 | 25 | public var debugDescription: String { 26 | return """ 27 | 28 | Color: \(String(describing: _color)) 29 | Population: \(population) 30 | """ 31 | } 32 | 33 | internal init(color: Color, population: Int) { 34 | self._color = color 35 | self.population = population 36 | } 37 | 38 | internal let _color: Color 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Source/Target.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Target.swift 3 | // Palette 4 | // 5 | // Created by Egor Snitsar on 05.08.2019. 6 | // Copyright © 2019 Egor Snitsar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension Target { 12 | 13 | public static let lightVibrant: Target = { 14 | var result = Target() 15 | result.setDefaultLightLightnessValues() 16 | result.setDefaultVibrantSaturationValues() 17 | 18 | return result 19 | }() 20 | 21 | public static let vibrant: Target = { 22 | var result = Target() 23 | result.setDefaultNormalLightnessValues() 24 | result.setDefaultVibrantSaturationValues() 25 | 26 | return result 27 | }() 28 | 29 | public static let darkVibrant: Target = { 30 | var result = Target() 31 | result.setDefaultDarkLightnessValues() 32 | result.setDefaultVibrantSaturationValues() 33 | 34 | return result 35 | }() 36 | 37 | public static let lightMuted: Target = { 38 | var result = Target() 39 | result.setDefaultLightLightnessValues() 40 | result.setDefaultMutedSaturationValues() 41 | 42 | return result 43 | }() 44 | 45 | public static let muted: Target = { 46 | var result = Target() 47 | result.setDefaultNormalLightnessValues() 48 | result.setDefaultMutedSaturationValues() 49 | 50 | return result 51 | }() 52 | 53 | public static let darkMuted: Target = { 54 | var result = Target() 55 | result.setDefaultDarkLightnessValues() 56 | result.setDefaultMutedSaturationValues() 57 | 58 | return result 59 | }() 60 | } 61 | 62 | public final class Target: Hashable { 63 | 64 | // MARK: - Public 65 | 66 | public internal(set) var minimumSaturation: CGFloat { 67 | get { 68 | return saturation.min 69 | } 70 | set { 71 | saturation.min = newValue 72 | } 73 | } 74 | 75 | public internal(set) var targetSaturation: CGFloat { 76 | get { 77 | return saturation.target 78 | } 79 | set { 80 | saturation.target = newValue 81 | } 82 | } 83 | 84 | public internal(set) var maximumSaturation: CGFloat { 85 | get { 86 | return saturation.max 87 | } 88 | set { 89 | saturation.max = newValue 90 | } 91 | } 92 | 93 | public internal(set) var minimumLightness: CGFloat { 94 | get { 95 | return lightness.min 96 | } 97 | set { 98 | lightness.min = newValue 99 | } 100 | } 101 | 102 | public internal(set) var targetLightness: CGFloat { 103 | get { 104 | return lightness.target 105 | } 106 | set { 107 | lightness.target = newValue 108 | } 109 | } 110 | 111 | public internal(set) var maximumLightness: CGFloat { 112 | get { 113 | return lightness.max 114 | } 115 | set { 116 | lightness.max = newValue 117 | } 118 | } 119 | 120 | public internal(set) var saturationWeight: CGFloat { 121 | get { 122 | return weights.saturation 123 | } 124 | set { 125 | weights.saturation = newValue 126 | } 127 | } 128 | 129 | public internal(set) var lightnessWeight: CGFloat { 130 | get { 131 | return weights.lightness 132 | } 133 | set { 134 | weights.lightness = newValue 135 | } 136 | } 137 | 138 | public internal(set) var populationWeight: CGFloat { 139 | get { 140 | return weights.population 141 | } 142 | set { 143 | weights.population = newValue 144 | } 145 | } 146 | 147 | public internal(set) var isExclusive: Bool = true 148 | 149 | // MARK: - Internal 150 | 151 | internal init() {} 152 | 153 | internal init(_ other: Target) { 154 | self.saturation = other.saturation 155 | self.lightness = other.lightness 156 | self.weights = other.weights 157 | } 158 | 159 | // MARK: - Hashable 160 | 161 | public static func == (lhs: Target, rhs: Target) -> Bool { 162 | return lhs.saturation == rhs.saturation && lhs.lightness == rhs.lightness 163 | } 164 | 165 | public func hash(into hasher: inout Hasher) { 166 | hasher.combine(saturation) 167 | hasher.combine(lightness) 168 | } 169 | 170 | // MARK: - Internal 171 | 172 | internal func normalizeWeights() { 173 | let sum = weights.saturation + weights.lightness + weights.population 174 | 175 | guard sum > 0 else { 176 | return 177 | } 178 | 179 | weights.saturation /= sum 180 | weights.lightness /= sum 181 | weights.population /= sum 182 | } 183 | 184 | // MARK: - Private 185 | 186 | private struct Value: Hashable { 187 | var min: CGFloat = 0.0 188 | var target: CGFloat = 0.5 189 | var max: CGFloat = 1.0 190 | } 191 | 192 | private struct Weights: Hashable { 193 | var saturation: CGFloat = 0.24 194 | var lightness: CGFloat = 0.52 195 | var population: CGFloat = 0.24 196 | } 197 | 198 | private var saturation = Value() 199 | private var lightness = Value() 200 | private var weights = Weights() 201 | 202 | private func setDefaultLightLightnessValues() { 203 | lightness.min = 0.55 204 | lightness.target = 0.74 205 | } 206 | 207 | private func setDefaultNormalLightnessValues() { 208 | lightness.min = 0.3 209 | lightness.target = 0.5 210 | lightness.max = 0.7 211 | } 212 | 213 | private func setDefaultDarkLightnessValues() { 214 | lightness.target = 0.26 215 | lightness.max = 0.45 216 | } 217 | 218 | private func setDefaultVibrantSaturationValues() { 219 | saturation.min = 0.35 220 | saturation.target = 1.0 221 | } 222 | 223 | private func setDefaultMutedSaturationValues() { 224 | saturation.target = 0.3 225 | saturation.max = 0.4 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /Source/TargetBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TargetBuilder.swift 3 | // Palette 4 | // 5 | // Created by Egor Snitsar on 10.08.2019. 6 | // Copyright © 2019 Egor Snitsar. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Target { 12 | 13 | public final class Builder { 14 | 15 | public init() { 16 | self.target = Target() 17 | } 18 | 19 | public init(_ target: Target) { 20 | self.target = Target(target) 21 | } 22 | 23 | public func with(minimumSaturation: CGFloat) -> Builder { 24 | target.minimumSaturation = minimumSaturation 25 | 26 | return self 27 | } 28 | 29 | public func with(targetSaturation: CGFloat) -> Builder { 30 | target.targetSaturation = targetSaturation 31 | 32 | return self 33 | } 34 | 35 | public func with(maximumSaturation: CGFloat) -> Builder { 36 | target.maximumSaturation = maximumSaturation 37 | 38 | return self 39 | } 40 | 41 | public func with(minimumLightness: CGFloat) -> Builder { 42 | target.minimumLightness = minimumLightness 43 | 44 | return self 45 | } 46 | 47 | public func with(targetLightness: CGFloat) -> Builder { 48 | target.targetLightness = targetLightness 49 | 50 | return self 51 | } 52 | 53 | public func with(maximumLightness: CGFloat) -> Builder { 54 | target.maximumLightness = maximumLightness 55 | 56 | return self 57 | } 58 | 59 | public func with(saturationWeight: CGFloat) -> Builder { 60 | target.saturationWeight = saturationWeight 61 | 62 | return self 63 | } 64 | 65 | public func with(lightnessWeight: CGFloat) -> Builder { 66 | target.lightnessWeight = lightnessWeight 67 | 68 | return self 69 | } 70 | 71 | public func with(populationWeight: CGFloat) -> Builder { 72 | target.populationWeight = populationWeight 73 | 74 | return self 75 | } 76 | 77 | public func with(exclusive: Bool) -> Builder { 78 | target.isExclusive = exclusive 79 | 80 | return self 81 | } 82 | 83 | public func build() -> Target { 84 | return target 85 | } 86 | 87 | private let target: Target 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Source/UIImage+Palette.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Palette.swift 3 | // Palette 4 | // 5 | // Created by Egor Snitsar on 09.08.2019. 6 | // Copyright © 2019 Egor Snitsar. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension UIImage { 12 | 13 | public func createPalette() -> Palette { 14 | return Palette.from(image: self).generate() 15 | } 16 | 17 | public func createPalette(_ completion: @escaping (Palette) -> Void) { 18 | return Palette.from(image: self).generate(completion) 19 | } 20 | } 21 | --------------------------------------------------------------------------------