├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ ├── DominantColors.xcscheme │ └── DominantColorsTests.xcscheme ├── DominantColors.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Example ├── .swiftpm │ └── xcode │ │ └── package.xcworkspace │ │ └── contents.xcworkspacedata ├── MacOSPreview │ ├── MacOSPreview.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── MacOSPreview │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── BladeRunner2049.imageset │ │ │ ├── Contents.json │ │ │ └── bladerunner056.jpg │ │ ├── Contents.json │ │ └── NeonDemon.imageset │ │ │ ├── Contents.json │ │ │ └── neondemon060.jpg │ │ ├── ContentView.swift │ │ ├── MacOSPreview.entitlements │ │ ├── MacOSPreviewApp.swift │ │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ ├── ComeTogether.imageset │ │ │ ├── ComeTogether.jpg │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── LittleMissSunshine.imageset │ │ │ ├── Contents.json │ │ │ └── LittleMissSunshine.jpg │ │ │ ├── TheLifeAquaticWithSteveZissou.imageset │ │ │ ├── Contents.json │ │ │ └── WaterLife1.jpg │ │ │ ├── blackwhite.imageset │ │ │ ├── 62 (896).jpg │ │ │ └── Contents.json │ │ │ ├── bladerunner042.imageset │ │ │ ├── Contents.json │ │ │ └── bladerunner042.jpg │ │ │ └── bladerunner056.imageset │ │ │ ├── Contents.json │ │ │ └── bladerunner056.jpg │ │ └── PreviewMacOS.swift ├── Package.swift └── iOSDominantColorsPreview │ ├── iOSDominantColorsPreview.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ └── iOSDominantColorsPreview │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── GragDavid.imageset │ │ ├── 329861783-9436f044-a285-492a-9ff4-309438bb327d.jpeg │ │ └── Contents.json │ └── The_Weeknd_-_Starboy.imageset │ │ ├── Contents.json │ │ └── The_Weeknd_-_Starboy.png │ ├── ColorSettingsView.swift │ ├── ContentView.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ ├── ComeTogether.imageset │ │ ├── ComeTogether.jpg │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── LittleMissSunshine.imageset │ │ ├── Contents.json │ │ └── LittleMissSunshine.jpg │ │ ├── TheLifeAquaticWithSteveZissou.imageset │ │ ├── Contents.json │ │ └── WaterLife1.jpg │ │ ├── blackwhite.imageset │ │ ├── 62 (896).jpg │ │ └── Contents.json │ │ ├── bladerunner042.imageset │ │ ├── Contents.json │ │ └── bladerunner042.jpg │ │ └── bladerunner056.imageset │ │ ├── Contents.json │ │ └── bladerunner056.jpg │ ├── PreviewiOS.swift │ ├── TitleView.swift │ └── iOSDominantColorsPreviewApp.swift ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── DominantColors │ ├── AverageColor+UIColor.swift │ ├── AverageColor.swift │ ├── ColorFrequency.swift │ ├── ColorPalette │ └── ContrastColors.swift │ ├── Colors │ ├── ColorShade.swift │ ├── HSL.swift │ ├── Hex.swift │ ├── Lab.swift │ ├── RGB.swift │ ├── RGB255.swift │ └── XYZ.swift │ ├── ComplementaryColor.swift │ ├── ContrastRatio.swift │ ├── DeltaEFormula.swift │ ├── Difference.swift │ ├── DominantColorAlgorithm.swift │ ├── DominantColorQuality.swift │ ├── DominantColors+CIAreaAverage.swift │ ├── DominantColors+CIE.swift │ ├── DominantColors+CIKMeans.swift │ ├── DominantColors+ColorResolved.swift │ ├── DominantColors+NSColor.swift │ ├── DominantColors+UIColor.swift │ ├── DominantColors.swift │ ├── Extensions │ ├── CGFloatExtensions.swift │ ├── CGImageExtensions.swift │ ├── CGSizeExtensions.swift │ ├── NSImageExtension.swift │ └── UIImageExtension.swift │ ├── GradientColors.swift │ ├── ImageColorError.swift │ ├── ImageFIlter.swift │ ├── ImageTrimAlpha.swift │ └── RelativeLuminance.swift └── Tests └── DominantColorsTests ├── AverageColorTests.swift ├── CGFloatExtensionsTests.swift ├── CGSizeExtensionsTests.swift ├── ColorDifferenceTest.swift ├── ColorFrequencyTests.swift ├── ColorShadeTests.swift ├── ComparisonTests.swift ├── ComplementaryColorTests.swift ├── ContrastColorsTests.swift ├── ContrastRatioTests.swift ├── DominantColorQualityTests.swift ├── DominantColorsTests.swift ├── ExtractColorsTests.swift ├── FIlterBlackWhiteTests.swift ├── FilterImageTests.swift ├── GradientColorsTests.swift ├── HSLTests.swift ├── HexTests.swift ├── LabTests.swift ├── Media.xcassets ├── BlackGrayColorSpace.imageset │ ├── BlackGrayColorSpace.png │ └── Contents.json ├── Black_Shades.imageset │ ├── Black_Shades.png │ └── Contents.json ├── Black_White_Red_Green_Blue_Grey.imageset │ ├── Black_White_Red_Green_Blue_Grey.png │ └── Contents.json ├── Black_White_Square.imageset │ ├── Black_White_Square.jpg │ └── Contents.json ├── Blue_Green_Square.imageset │ ├── Blue_Green_Square.jpg │ └── Contents.json ├── Blue_Shades.imageset │ ├── Color_icon_blue.svg.png │ └── Contents.json ├── Blue_Square_1x1.imageset │ ├── Blue_Square_1x1.jpg │ └── Contents.json ├── Brown_Shades.imageset │ ├── Brown_Shades.png │ └── Contents.json ├── Contents.json ├── GrayColorSpaceImage.imageset │ ├── Contents.json │ └── GrayColorSpaceImage.png ├── Green_Shades.imageset │ ├── Color_icon_green.png │ └── Contents.json ├── Green_Square.imageset │ ├── Contents.json │ └── Green_Square.png ├── LittleMissSunshine.imageset │ ├── Contents.json │ └── LittleMissSunshine.jpg ├── Orange_Shades.imageset │ ├── Color_icon_orange.png │ └── Contents.json ├── Pink_Shades.imageset │ ├── Contents.json │ └── Pink_Shades.png ├── Pixeleate_Image.imageset │ ├── Contents.json │ └── TestPixeleate.png ├── Purple_Square.imageset │ ├── Contents.json │ └── Purple_Square.jpg ├── Red_Green_Blue.imageset │ ├── Contents.json │ └── Red_Green_Blue.png ├── Red_Green_Blue_Black_Mini.imageset │ ├── Contents.json │ └── Red_Green_Blue_Black_Mini.png ├── Red_Green_Blue_Random.imageset │ ├── Contents.json │ └── Red_Green_Blue_Random.png ├── Red_Green_Blue_Random_Mini.imageset │ ├── Contents.json │ └── Red_Green_Blue_Random_Mini.png ├── Red_Shades.imageset │ ├── Color_icon_red.png │ └── Contents.json ├── ShadesGrayColorSpace.imageset │ ├── Contents.json │ └── ShadesGrayColorSpace.png ├── Test_Crop_Alpha.imageset │ ├── Contents.json │ └── TestCropAlpha.png ├── Test_Image_1.imageset │ ├── Contents.json │ └── Test_Image_1.jpeg ├── Test_Image_2.imageset │ ├── Contents.json │ └── Test_Image_2.jpeg ├── Test_Image_3.imageset │ ├── Contents.json │ └── Test_Image_3.jpg ├── Violet_Shades.imageset │ ├── Color_icon_violet.png │ └── Contents.json ├── WaterLife1.imageset │ ├── Contents.json │ └── WaterLife1.jpg └── Yellow_Shades.imageset │ ├── Contents.json │ └── Yellow_Shades.png ├── RGBTests.swift ├── RelativeLuminanceTests.swift └── XYZTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/DominantColors.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 62 | 68 | 69 | 70 | 71 | 72 | 82 | 83 | 89 | 90 | 96 | 97 | 98 | 99 | 101 | 102 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/DominantColorsTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 17 | 19 | 25 | 26 | 27 | 28 | 29 | 39 | 40 | 46 | 47 | 49 | 50 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /DominantColors.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /DominantColors.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "dominantcolors", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/DenDmitriev/DominantColors", 7 | "state" : { 8 | "revision" : "77b8fc7d3b24934e0a49869cf1183e9395dbaffe", 9 | "version" : "1.1.9" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Assets.xcassets/BladeRunner2049.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bladerunner056.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Assets.xcassets/BladeRunner2049.imageset/bladerunner056.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Example/MacOSPreview/MacOSPreview/Assets.xcassets/BladeRunner2049.imageset/bladerunner056.jpg -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Assets.xcassets/NeonDemon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "neondemon060.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Assets.xcassets/NeonDemon.imageset/neondemon060.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Example/MacOSPreview/MacOSPreview/Assets.xcassets/NeonDemon.imageset/neondemon060.jpg -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/MacOSPreview.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/MacOSPreviewApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MacOSPreviewApp.swift 3 | // MacOSPreview 4 | // 5 | // Created by Denis Dmitriev on 01.05.2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct MacOSPreviewApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/ComeTogether.imageset/ComeTogether.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/ComeTogether.imageset/ComeTogether.jpg -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/ComeTogether.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ComeTogether.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/LittleMissSunshine.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LittleMissSunshine.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/LittleMissSunshine.imageset/LittleMissSunshine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/LittleMissSunshine.imageset/LittleMissSunshine.jpg -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/TheLifeAquaticWithSteveZissou.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "WaterLife1.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/TheLifeAquaticWithSteveZissou.imageset/WaterLife1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/TheLifeAquaticWithSteveZissou.imageset/WaterLife1.jpg -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/blackwhite.imageset/62 (896).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/blackwhite.imageset/62 (896).jpg -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/blackwhite.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "62 (896).jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/bladerunner042.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bladerunner042.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/bladerunner042.imageset/bladerunner042.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/bladerunner042.imageset/bladerunner042.jpg -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/bladerunner056.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bladerunner056.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/bladerunner056.imageset/bladerunner056.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Example/MacOSPreview/MacOSPreview/Preview Content/Preview Assets.xcassets/bladerunner056.imageset/bladerunner056.jpg -------------------------------------------------------------------------------- /Example/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Example", 7 | products: [], 8 | targets: [] 9 | ) 10 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "dominantcolors", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/DenDmitriev/DominantColors", 7 | "state" : { 8 | "revision" : "77b8fc7d3b24934e0a49869cf1183e9395dbaffe", 9 | "version" : "1.1.9" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Assets.xcassets/GragDavid.imageset/329861783-9436f044-a285-492a-9ff4-309438bb327d.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Assets.xcassets/GragDavid.imageset/329861783-9436f044-a285-492a-9ff4-309438bb327d.jpeg -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Assets.xcassets/GragDavid.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "329861783-9436f044-a285-492a-9ff4-309438bb327d.jpeg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Assets.xcassets/The_Weeknd_-_Starboy.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "The_Weeknd_-_Starboy.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Assets.xcassets/The_Weeknd_-_Starboy.imageset/The_Weeknd_-_Starboy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Assets.xcassets/The_Weeknd_-_Starboy.imageset/The_Weeknd_-_Starboy.png -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/ColorSettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorSettingsView.swift 3 | // iOSDominantColorsPreview 4 | // 5 | // Created by Denis Dmitriev on 11.05.2024. 6 | // 7 | 8 | import SwiftUI 9 | import DominantColors 10 | 11 | struct ColorSettingsView: View { 12 | 13 | @Environment(\.dismiss) var dissmis 14 | 15 | // Settings 16 | @Binding var formula: DeltaEFormula 17 | @Binding var countColor: Int 18 | @Binding var quality: DominantColorQuality 19 | 20 | // Options 21 | @Binding var removeBlack: Bool 22 | @Binding var removeWhite: Bool 23 | @Binding var removeGray: Bool 24 | 25 | // Order 26 | @Binding var sorting: DominantColors.Sort 27 | 28 | var body: some View { 29 | VStack(spacing: 0) { 30 | HStack { 31 | Text("Settings") 32 | .font(.title3) 33 | } 34 | .frame(maxWidth: .infinity) 35 | .overlay(alignment: .trailing) { 36 | Button { 37 | dissmis() 38 | } label: { 39 | Image(systemName: "xmark") 40 | } 41 | .padding(4) 42 | } 43 | .padding(.top, 8) 44 | .padding(8) 45 | 46 | List { 47 | Section { 48 | HStack { 49 | Text("Number of colors") 50 | .frame(maxWidth: .infinity, alignment: .leading) 51 | TextField("Count", value: $countColor, format: .number) 52 | .keyboardType(.numberPad) 53 | .overlay(alignment: .bottom) { 54 | Rectangle() 55 | .fill(.primary) 56 | .frame(height: 0.5) 57 | } 58 | .frame(width: 40) 59 | Image(systemName: "number") 60 | .foregroundStyle(.secondary) 61 | } 62 | } 63 | 64 | Section("Color Difference") { 65 | Picker("ΔE Formula", selection: $formula) { 66 | ForEach(DeltaEFormula.allCases) { 67 | Text($0.method).tag($0) 68 | } 69 | } 70 | 71 | Picker("Quality", selection: $quality) { 72 | ForEach(DominantColorQuality.allCases, id: \.self) { 73 | Text($0.description).tag($0) 74 | } 75 | } 76 | 77 | Picker("Sorting", selection: $sorting) { 78 | ForEach(DominantColors.Sort.allCases) { 79 | Text($0.name).tag($0) 80 | } 81 | } 82 | } 83 | 84 | Section("Options") { 85 | Toggle("Remove black colors", isOn: $removeBlack) 86 | Toggle("Remove gray colors", isOn: $removeGray) 87 | Toggle("Remove white colors", isOn: $removeWhite) 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | #Preview { 95 | ColorSettingsView( 96 | formula: .constant(.CIE76), 97 | countColor: .constant(6), 98 | quality: .constant(.fair), 99 | removeBlack: .constant(false), 100 | removeWhite: .constant(false), 101 | removeGray: .constant(false), 102 | sorting: .constant(.frequency) 103 | ) 104 | .preferredColorScheme(.dark) 105 | } 106 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // iOSDominantColorsPreview 4 | // 5 | // Created by Denis Dmitriev on 01.05.2024. 6 | // 7 | 8 | import SwiftUI 9 | import PhotosUI 10 | import DominantColors 11 | 12 | struct ContentView: View { 13 | 14 | @State private var uiImage: UIImage? 15 | @State private var colors = [Color]() 16 | @State private var pickerItem: PhotosPickerItem? 17 | @State private var imageURL: URL? 18 | @State private var showingAlert = false 19 | @State private var showingSettings = false 20 | @Environment(\.verticalSizeClass) var verticalSizeClass 21 | @State private var showUI = true 22 | 23 | @State private var contrastColors: ContrastColors? 24 | 25 | // Settings 26 | @State private var formula: DeltaEFormula = .CIE94 27 | @State private var countColor: Int = 6 28 | @State private var quality: DominantColorQuality = .fair 29 | 30 | // Options 31 | @State private var removeBlack: Bool = true 32 | @State private var removeWhite: Bool = false 33 | @State private var removeGray: Bool = false 34 | 35 | // Order 36 | @State private var sorting: DominantColors.Sort = .frequency 37 | 38 | var body: some View { 39 | NavigationStack { 40 | VStack(spacing: .zero) { 41 | if let uiImage { 42 | Image(uiImage: uiImage) 43 | .resizable() 44 | .scaledToFit() 45 | .onTapGesture { 46 | showUI.toggle() 47 | } 48 | } else { 49 | placeholderImage 50 | } 51 | } 52 | .frame(maxWidth: .infinity, maxHeight: .infinity) 53 | .overlay(alignment: .bottom) { 54 | if showUI { 55 | TitleView(title: "Title", subtitle: "Subtitle", contrastColors: $contrastColors) 56 | .padding(.bottom, 140) 57 | } 58 | } 59 | .overlay(alignment: .bottom, content: { 60 | HStack(spacing: .zero) { 61 | if !colors.isEmpty { 62 | ForEach(Array(zip(colors.indices, colors)), id: \.0) { index, color in 63 | Rectangle() 64 | .fill(color) 65 | } 66 | } else if uiImage != nil { 67 | ProgressView() 68 | .tint(.white) 69 | } 70 | } 71 | .onChange(of: uiImage) { newImage in 72 | if let newImage { 73 | refreshColors(from: newImage) 74 | } 75 | } 76 | .frame(height: verticalSizeClass == .compact ? 50 : 100) 77 | }) 78 | .ignoresSafeArea() 79 | .toolbar { 80 | if showUI { 81 | ToolbarItem(placement: .topBarTrailing) { 82 | Button { 83 | showingAlert.toggle() 84 | } label: { 85 | Image(systemName: "link.circle.fill") 86 | } 87 | .alert("Paste image URL", isPresented: $showingAlert) { 88 | TextField("URL image", value: $imageURL, format: .url) 89 | .padding() 90 | 91 | Button("OK", action: { 92 | if let imageURL { 93 | Task { 94 | await loadImage(.network(url: imageURL)) 95 | } 96 | } 97 | }) 98 | } 99 | } 100 | 101 | ToolbarItem(placement: .topBarTrailing) { 102 | PhotosPicker(selection: $pickerItem, matching: .images) { 103 | Image(systemName: "photo.stack.fill") 104 | } 105 | .onChange(of: pickerItem) { pickerItem in 106 | Task { 107 | await loadImage(.gallery(pickerItem: pickerItem)) 108 | } 109 | } 110 | } 111 | 112 | ToolbarItem(placement: .primaryAction) { 113 | Button("Setting", systemImage: "gearshape.fill") { 114 | showingSettings.toggle() 115 | } 116 | } 117 | } 118 | } 119 | } 120 | .sheet(isPresented: $showingSettings, onDismiss: { 121 | if let uiImage { 122 | refreshColors(from: uiImage) 123 | } 124 | }, content: { 125 | ColorSettingsView( 126 | formula: $formula, 127 | countColor: $countColor, 128 | quality: $quality, 129 | removeBlack: $removeBlack, 130 | removeWhite: $removeWhite, 131 | removeGray: $removeGray, 132 | sorting: $sorting 133 | ) 134 | .presentationDetents([.fraction(0.4)]) 135 | }) 136 | .task { 137 | await loadImage(.asset(name: "ComeTogether")) 138 | } 139 | } 140 | 141 | private var placeholderImage: some View { 142 | Text("No image") 143 | .foregroundStyle(.gray) 144 | .frame(height: 300) 145 | } 146 | } 147 | 148 | extension ContentView { 149 | enum LoadImageType { 150 | case gallery(pickerItem: PhotosPickerItem?), network(url: URL), asset(name: String) 151 | } 152 | 153 | private func loadImage(_ type: LoadImageType) async { 154 | let resultImage: UIImage 155 | switch type { 156 | case .gallery(let pickerItem): 157 | guard let data = try? await pickerItem?.loadTransferable(type: Data.self), 158 | let uiImage = UIImage(data: data) 159 | else { return } 160 | 161 | resultImage = uiImage 162 | case .network(let url): 163 | guard let result = try? await URLSession.shared.data(from: url), 164 | let uiImage = UIImage(data: result.0) 165 | else { return } 166 | 167 | resultImage = uiImage 168 | case .asset(let name): 169 | guard let uiImage = UIImage(named: name) else { return } 170 | 171 | resultImage = uiImage 172 | } 173 | 174 | DispatchQueue.main.async { 175 | self.uiImage = resultImage 176 | } 177 | } 178 | 179 | private func refreshColors(from uiImage: UIImage){ 180 | colors.removeAll() 181 | 182 | Task { 183 | var options: [DominantColors.Options] = [] 184 | if removeBlack { options.append(.excludeBlack) } 185 | if removeWhite { options.append(.excludeWhite) } 186 | if removeGray { options.append(.excludeGray) } 187 | 188 | guard let uiColors = try? DominantColors.dominantColors( 189 | uiImage: uiImage, 190 | quality: quality, 191 | algorithm: formula, 192 | maxCount: countColor, 193 | options: options, 194 | sorting: sorting 195 | ) 196 | else { return } 197 | 198 | DispatchQueue.main.async { 199 | self.colors = uiColors.map({ Color(uiColor: $0) }) 200 | } 201 | 202 | let contrastColors = ContrastColors(colors: uiColors.map({ $0.cgColor })) 203 | 204 | DispatchQueue.main.async { 205 | self.contrastColors = contrastColors 206 | } 207 | } 208 | } 209 | } 210 | 211 | #Preview { 212 | ContentView() 213 | .preferredColorScheme(.dark) 214 | } 215 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/ComeTogether.imageset/ComeTogether.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/ComeTogether.imageset/ComeTogether.jpg -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/ComeTogether.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ComeTogether.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/LittleMissSunshine.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LittleMissSunshine.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/LittleMissSunshine.imageset/LittleMissSunshine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/LittleMissSunshine.imageset/LittleMissSunshine.jpg -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/TheLifeAquaticWithSteveZissou.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "WaterLife1.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/TheLifeAquaticWithSteveZissou.imageset/WaterLife1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/TheLifeAquaticWithSteveZissou.imageset/WaterLife1.jpg -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/blackwhite.imageset/62 (896).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/blackwhite.imageset/62 (896).jpg -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/blackwhite.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "62 (896).jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/bladerunner042.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bladerunner042.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/bladerunner042.imageset/bladerunner042.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/bladerunner042.imageset/bladerunner042.jpg -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/bladerunner056.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bladerunner056.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/bladerunner056.imageset/bladerunner056.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Example/iOSDominantColorsPreview/iOSDominantColorsPreview/Preview Content/Preview Assets.xcassets/bladerunner056.imageset/bladerunner056.jpg -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/TitleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleView.swift 3 | // iOSDominantColorsPreview 4 | // 5 | // Created by Denis Dmitriev on 13.05.2024. 6 | // 7 | 8 | import SwiftUI 9 | import DominantColors 10 | 11 | struct TitleView: View { 12 | @State var title: String 13 | @State var subtitle: String 14 | @Binding var contrastColors: ContrastColors? 15 | 16 | var body: some View { 17 | VStack { 18 | if let contrastColors { 19 | VStack { 20 | Text(title) 21 | .font(.largeTitle) 22 | .foregroundStyle(Color(cgColor: contrastColors.primary)) 23 | 24 | Text(subtitle) 25 | .font(.title3) 26 | .foregroundStyle(Color(cgColor: contrastColors.secondary ?? CGColor(gray: 0, alpha: 0))) 27 | } 28 | .padding() 29 | .background(Color(cgColor: contrastColors.background)) 30 | } else { 31 | EmptyView() 32 | } 33 | } 34 | .clipShape(RoundedRectangle(cornerRadius: 16)) 35 | } 36 | } 37 | 38 | #Preview { 39 | VStack { 40 | TitleView(title: "Title", subtitle: "Subtitle", contrastColors: .constant(ContrastColors(colors: [.black, .white, .init(gray: 0.5, alpha: 1)]))) 41 | 42 | TitleView(title: "Title", subtitle: "Subtitle", contrastColors: .constant(nil)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Example/iOSDominantColorsPreview/iOSDominantColorsPreview/iOSDominantColorsPreviewApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // iOSDominantColorsPreviewApp.swift 3 | // iOSDominantColorsPreview 4 | // 5 | // Created by Denis Dmitriev on 01.05.2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct iOSDominantColorsPreviewApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | .preferredColorScheme(.dark) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Denis Dmitriev 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 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "DCLib", 8 | platforms: [ 9 | .macOS(.v11), 10 | .iOS(.v13), 11 | .tvOS(.v13), 12 | .watchOS(.v6) 13 | ], 14 | products: [ 15 | // Products define the executables and libraries a package produces, and make them visible to other packages. 16 | .library( 17 | name: "DominantColors", 18 | targets: ["DominantColors"]), 19 | ], 20 | dependencies: [ 21 | // Dependencies declare other packages that this package depends on. 22 | // .package(url: /* package url */, from: "1.0.0"), 23 | ], 24 | targets: [ 25 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 26 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 27 | .target( 28 | name: "DominantColors", 29 | dependencies: [], 30 | path: "Sources/DominantColors"), 31 | .testTarget( 32 | name: "DominantColorsTests", 33 | dependencies: ["DominantColors"], 34 | resources: [.process("Media.xcassets")]), 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /Sources/DominantColors/AverageColor+UIColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AverageColor+UIColor.swift 3 | // 4 | // 5 | // Created by Saffet Emin Reisoğlu on 8/29/24. 6 | // 7 | 8 | #if os(iOS) 9 | import UIKit 10 | 11 | @available(iOS 2.0, *) 12 | extension AverageColor { 13 | 14 | public static func averageColor(uiImage: UIImage) throws -> UIColor { 15 | guard let cgImage = uiImage.cgImage else { throw ImageColorError.cgImageFailure } 16 | return .init(cgColor: try averageColor(image: cgImage)) 17 | } 18 | } 19 | #endif 20 | -------------------------------------------------------------------------------- /Sources/DominantColors/AverageColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AverageColor.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 04.09.2023. 6 | // 7 | 8 | import CoreImage 9 | 10 | public class AverageColor { 11 | 12 | /// Computes the average color of the image. 13 | static func averageColor(image: CGImage) throws -> CGColor { 14 | let ciImage = CIImage(cgImage: image) 15 | 16 | guard let areaAverageFilter = CIFilter(name: "CIAreaAverage") else { 17 | fatalError("Could not create `CIAreaAverage` filter.") 18 | } 19 | 20 | areaAverageFilter.setValue(ciImage, forKey: kCIInputImageKey) 21 | areaAverageFilter.setValue(CIVector(cgRect: ciImage.extent), forKey: "inputExtent") 22 | 23 | guard let outputImage = areaAverageFilter.outputImage else { 24 | throw ImageColorError.outputImageFailure 25 | } 26 | 27 | let context = CIContext() 28 | var bitmap = [UInt8](repeating: 0, count: 4) 29 | 30 | context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: CIFormat.RGBA8, colorSpace: CGColorSpaceCreateDeviceRGB()) 31 | 32 | let red: CGFloat = CGFloat(bitmap[0]) / 255.0 33 | let green: CGFloat = CGFloat(bitmap[1]) / 255.0 34 | let blue: CGFloat = CGFloat(bitmap[2]) / 255.0 35 | let alpha: CGFloat = CGFloat(bitmap[3]) / 255.0 36 | 37 | let components: [CGFloat] = [red, green, blue, alpha] 38 | 39 | guard 40 | let colorSpace = image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB), 41 | let averageColor = CGColor(colorSpace: colorSpace, components: components) 42 | else { throw ImageColorError.cgColorFailure } 43 | 44 | return averageColor 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/DominantColors/ColorFrequency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorFrequency.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 23.04.2024. 6 | // 7 | 8 | import CoreGraphics.CGColor 9 | 10 | /// A simple structure containing a color, and a frequency. 11 | public class ColorFrequency: CustomStringConvertible { 12 | /// A simple `CGColor` instance. 13 | public var color: CGColor 14 | 15 | /// The frequency of the color. 16 | /// 17 | /// Shows how many times a color is present in an image. 18 | public var frequency: CGFloat 19 | 20 | /// Parameter of color importance by frequency and normal brightness. 21 | /// 22 | /// Calculated based on their human sensitivity to normal color multiplied by the number of `frequency` pixels of this color in the image, 23 | /// where normal means an average brightness of 50% and saturation of 75% in HSL color. 24 | /// The closer the brightness is to 50% in HSL color, the greater the coefficient will be. Lighter or darker colors will produce a lower ratio. 25 | /// The closer the saturation is to 75% in an HSL color, the higher the coefficient will be. Paler or oversaturated colors will produce a lower ratio. 26 | /// For example, a color HSL(0, 75, 50) with a frequency of 100 pixels in an image will receive `100 * 1 * 1 = 100`. 27 | public var normal: CGFloat { 28 | return frequency * normalLightnessFactor * normalSaturationFactor 29 | } 30 | 31 | public var shade: ColorShade 32 | 33 | public var description: String { 34 | return "Color: \(shade.title) (\(color)) - Frequency: \(frequency)" 35 | } 36 | 37 | init(color: CGColor, count: CGFloat) { 38 | self.frequency = count 39 | self.color = color 40 | self.shade = ColorShade(cgColor: color) 41 | } 42 | 43 | 44 | /// Normal brightness parameter. 45 | /// 46 | /// Has a size from 0 to 1. 47 | /// 48 | /// Calculated based on their sensitivity to normal color brightness by a person. 49 | /// Normal brightness is assumed to be 50% brightness in HSL color. 50 | /// The closer the brightness is to 50% in HSL color, the greater the coefficient will be. Very light or dark colors will give a lower ratio. 51 | /// For example, the color `HSL(0, 75, 50)` will receive `1`, and the `HSL(0, 25, 75)` color will receive `0.5`. 52 | var normalLightnessFactor: CGFloat { 53 | let lightnessFactor: CGFloat 54 | if color.lightness >= 50 { 55 | lightnessFactor = (50 - (color.lightness - 50)) / 50 56 | } else { 57 | lightnessFactor = (50 - (50 - color.lightness)) / 50 58 | } 59 | return lightnessFactor.rounded(.toNearestOrAwayFromZero, precision: 100) 60 | } 61 | 62 | /// Normal saturation parameter. 63 | /// 64 | /// Has a size from 0 to 1. 65 | /// 66 | /// Calculated based on their susceptibility to normal human color saturation. 67 | /// Normal saturation is assumed to be 75% saturation in HSL color. 68 | /// The closer the saturation is to 75% in an HSL color, the higher the coefficient will be. Paler or oversaturated colors will produce a lower ratio. 69 | /// For example, the color `HSL(0, 75, 50)` will receive `1`, and the `HSL(0, 25, 75)` color will receive `0.33`. 70 | var normalSaturationFactor: CGFloat { 71 | let saturationFactor: CGFloat 72 | if color.saturation >= 75 { 73 | saturationFactor = (75 - (color.saturation - 75)) / 75 74 | } else { 75 | saturationFactor = (75 - (75 - color.saturation)) / 75 76 | } 77 | return saturationFactor.rounded(.toNearestOrAwayFromZero, precision: 100) 78 | } 79 | } 80 | 81 | extension ColorFrequency: Hashable { 82 | public func hash(into hasher: inout Hasher) { 83 | hasher.combine(color.hashValue + Int(frequency)) 84 | } 85 | 86 | public static func == (lhs: ColorFrequency, rhs: ColorFrequency) -> Bool { 87 | lhs.color == rhs.color && 88 | lhs.frequency == rhs.frequency 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/DominantColors/ColorPalette/ContrastColors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContrastColors.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 12.05.2024. 6 | // 7 | 8 | import CoreGraphics.CGColor 9 | 10 | /// A simple structure used to represent color palettes. 11 | public struct ContrastColors { 12 | 13 | /// The color that should be used as a background. 14 | public let background: CGColor 15 | 16 | /// The color that should be used as the primary detail. For example the main text. 17 | public let primary: CGColor 18 | 19 | /// The color that should be used as the secondary detail. For example text that isn't as important as the one represented by the primary property. 20 | public let secondary: CGColor? 21 | 22 | /// Initializes a coherent color palette based on the passed in colors. 23 | /// The colors should be sorted by order of importance, where the first color is the most important. 24 | /// This makes it easy to generate palettes from a collection of colors. 25 | /// 26 | /// - Parameters: 27 | /// - orderedColors: The colors that will be used to generate the color palette. The first color is considered the most important one. 28 | /// - darkBackground: Whether the color palette is required to have a dark background. If set to false, the background can be dark or bright. 29 | /// - ignoreContrastRatio: Whether the color palette should ignore the contrast ratio between the different colors. It is recommended to set this value to `false` (default) if the color palette will be used to display text. 30 | public init?(orderedColors: [CGColor], darkBackground: Bool = true, ignoreContrastRatio: Bool = false) { 31 | guard orderedColors.count > 1 else { 32 | return nil 33 | } 34 | 35 | var backgroundColor: CGColor 36 | if darkBackground { 37 | guard let darkestOrderedColor = orderedColors.first(where: { color -> Bool in 38 | return color.relativeLuminance < 0.5 39 | }) else { 40 | return nil 41 | } 42 | backgroundColor = darkestOrderedColor 43 | } else { 44 | backgroundColor = orderedColors.first! 45 | } 46 | 47 | var primaryColor: CGColor? 48 | var secondaryColor: CGColor? 49 | if !ignoreContrastRatio { 50 | orderedColors.forEach { (color) in 51 | guard color != backgroundColor else { return } 52 | 53 | let contrastRatio = backgroundColor.contrastRatio(with: color) 54 | switch contrastRatio { 55 | case .acceptable, .acceptableForLargeText: 56 | if primaryColor != nil { 57 | secondaryColor = nil 58 | } else { 59 | primaryColor = color 60 | } 61 | default: 62 | return 63 | } 64 | } 65 | } else { 66 | orderedColors.forEach { (color) in 67 | guard color != backgroundColor else { return } 68 | if primaryColor == nil { 69 | primaryColor = color 70 | } else { 71 | secondaryColor = color 72 | } 73 | } 74 | } 75 | 76 | guard let primary = primaryColor else { return nil } 77 | self.background = backgroundColor 78 | self.primary = primary 79 | self.secondary = secondaryColor 80 | } 81 | 82 | /// Initializes a coherant color palette based on the passed in colors. 83 | /// This makes it easy to generate palettes from a collection of colors. 84 | /// 85 | /// - Parameters: 86 | /// - colors: The colors that will be used to generate the color palette. The best colors will be selected to have a color palette with enough contrast. At least two colors should be passed in. 87 | /// - darkBackground: Whether the color palette is required to have a dark background. If set to false, the background can be dark or bright. 88 | /// - ignoreContrastRatio: Whether the color palette should ignore the contrast ratio between the different colors. It is recommended to set this value to `false` (default) if the color palette will be used to display text. 89 | public init?(colors: [CGColor], darkBackground: Bool = true, ignoreContrastRatio: Bool = false) { 90 | guard colors.count > 1 else { 91 | return nil 92 | } 93 | 94 | var darkestColor: CGColor? 95 | var brightestColor: CGColor? 96 | 97 | colors.forEach { (color) in 98 | if color.relativeLuminance < darkestColor?.relativeLuminance ?? .greatestFiniteMagnitude { 99 | darkestColor = color 100 | } 101 | 102 | if color.relativeLuminance > brightestColor?.relativeLuminance ?? .leastNormalMagnitude { 103 | brightestColor = color 104 | } 105 | } 106 | guard let darkestColor = darkestColor, 107 | let brightestColor = brightestColor 108 | else { return nil } 109 | 110 | if !ignoreContrastRatio { 111 | let backgroundPrimaryContrastRatio = darkestColor.contrastRatio(with: brightestColor) 112 | switch backgroundPrimaryContrastRatio { 113 | case .acceptable, .acceptableForLargeText: 114 | break 115 | default: 116 | return nil 117 | } 118 | } 119 | 120 | let backgroundColor = darkBackground ? darkestColor : brightestColor 121 | let primaryColor = darkBackground ? brightestColor : darkestColor 122 | 123 | let secondaryColor = colors.first { (color) -> Bool in 124 | if !ignoreContrastRatio { 125 | let backgroundColorConstratRatio = color.contrastRatio(with: backgroundColor) 126 | switch backgroundColorConstratRatio { 127 | case .acceptable , .acceptableForLargeText: 128 | break 129 | default: return false 130 | } 131 | } 132 | 133 | let backgroundColorDifference = color.difference(from: backgroundColor) 134 | switch backgroundColorDifference { 135 | case .different, .far: 136 | let primaryColorDifference = color.difference(from: primaryColor) 137 | switch primaryColorDifference { 138 | case .near, .different, .far: 139 | return true 140 | default: 141 | return false 142 | } 143 | default: 144 | return false 145 | } 146 | } 147 | 148 | self.background = backgroundColor 149 | self.primary = primaryColor 150 | self.secondary = secondaryColor 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /Sources/DominantColors/Colors/HSL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HSL.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 05.09.2023. 6 | // 7 | 8 | import CoreImage 9 | 10 | public struct HSL { 11 | let hue: Double 12 | let saturation: Double 13 | let lightness: Double 14 | 15 | init?(cgColor: CGColor) { 16 | let red = UInt8(cgColor.red255) 17 | let green = UInt8(cgColor.green255) 18 | let blue = UInt8(cgColor.blue255) 19 | 20 | let hsl = HSLCalculator.convert(red: red, green: green, blue: blue) 21 | 22 | self.hue = hsl.hue 23 | self.saturation = hsl.saturation 24 | self.lightness = hsl.lightness 25 | } 26 | 27 | init(hue: Double, saturation: Double, lightness: Double) { 28 | self.hue = hue 29 | self.saturation = saturation 30 | self.lightness = lightness 31 | } 32 | 33 | var cgColor: CGColor { 34 | HSLCalculator.convert(hue: hue, saturation: saturation, lightness: lightness) 35 | } 36 | } 37 | 38 | struct HSLCalculator { 39 | 40 | /// Input RGB color with 0...255 color channels 41 | static func convert(red: UInt8, green: UInt8, blue: UInt8) -> HSL { 42 | let red = Double(red) 43 | let green = Double(green) 44 | let blue = Double(blue) 45 | let minColor = min(red, green, blue) 46 | let maxColor = max(red, green, blue) 47 | 48 | let lightness = 1/2 * (maxColor + minColor) / 255.0 49 | 50 | let delta = (maxColor - minColor) / 255.0 51 | 52 | let saturation: Double 53 | 54 | switch lightness { 55 | case 0: 56 | saturation = 0.0 57 | case 1: 58 | saturation = 0.0 59 | default: 60 | saturation = delta / (1 - abs(2 * lightness - 1)) 61 | } 62 | 63 | let hue: Double 64 | if (delta == 0) { 65 | hue = 0 66 | } else { 67 | switch(maxColor) { 68 | case red: 69 | let segment = (green - blue) / (delta * 255) 70 | var shift = 0 / 60.0 // R° / (360° / hex sides) 71 | if segment < 0 { // hue > 180, full rotation 72 | shift = 360 / 60 // R° / (360° / hex sides) 73 | } 74 | hue = segment + shift 75 | case green: 76 | let segment = (blue - red) / (delta * 255) 77 | let shift = 120.0 / 60.0 // G° / (360° / hex sides) 78 | hue = segment + shift 79 | case blue: 80 | let segment = (red - green) / (delta * 255) 81 | let shift = 240.0 / 60.0 // B° / (360° / hex sides) 82 | hue = segment + shift 83 | default: 84 | hue = .zero 85 | } 86 | } 87 | 88 | return HSL(hue: hue * 60, saturation: saturation * 100, lightness: lightness * 100) 89 | } 90 | 91 | /// Convert HSL to CGColor color 92 | /// Converts an HSL color value to RGB. . 93 | /// Assumes h, s, and l are contained in the set [0, 1] and returns RGB in the set [0, 255]. 94 | /// 95 | /// - Parameters: 96 | /// - HUE: 0...360 degree in color circle. 97 | /// - Saturation: 0...100 percent. 98 | /// - Lightness: 0...100 percent.. 99 | /// 100 | /// - Returns:`CGColor` color in sRGB color space. 101 | /// 102 | /// [Conversion formula.](http://en.wikipedia.org/wiki/HSL_color_space) 103 | static func convert(hue: CGFloat, saturation: CGFloat, lightness: CGFloat) -> CGColor { 104 | guard saturation != .zero else { 105 | let ligtness = lightness / 100 106 | return CGColor(srgbRed: ligtness, green: ligtness, blue: ligtness, alpha: 1.0) 107 | } 108 | 109 | let hue = hue / 360 110 | let saturation = saturation / 100 111 | let lightness = lightness / 100 112 | 113 | let q = lightness < 0.5 ? lightness * (1 + saturation) : lightness + saturation - lightness * saturation 114 | let p = 2 * lightness - q 115 | let r = hue2rgb(p, q, hue + 1/3) 116 | let g = hue2rgb(p, q, hue) 117 | let b = hue2rgb(p, q, hue - 1/3) 118 | 119 | return CGColor(srgbRed: r, green: g, blue: b, alpha: 1.0) 120 | } 121 | 122 | /// Converts an HUE to red, green or blue. 123 | /// - Returns: `CGFloat` in 0...1. 124 | static private func hue2rgb(_ p: CGFloat, _ q: CGFloat, _ t: CGFloat) -> CGFloat { 125 | var t = t 126 | 127 | if t < 0 { t += 1 } 128 | if t > 1 { t -= 1 } 129 | 130 | if t < 1/6 { 131 | return p + (q - p) * 6 * t 132 | } 133 | 134 | if t < 1/2 { 135 | return q 136 | } 137 | 138 | if t < 2/3 { 139 | return p + (q - p) * (2/3 - t) * 6 140 | } 141 | 142 | return p 143 | } 144 | } 145 | 146 | extension CGColor { 147 | public var hue: CGFloat { 148 | return hsl.hue 149 | } 150 | public var saturation: CGFloat { 151 | return hsl.saturation 152 | } 153 | public var lightness: CGFloat { 154 | return hsl.lightness 155 | } 156 | 157 | var hsl: HSL { 158 | return HSLCalculator.convert(red: UInt8(self.red255), green: UInt8(self.green255), blue: UInt8(self.blue255)) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Sources/DominantColors/Colors/Hex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Hex.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 04.09.2023. 6 | // 7 | 8 | import CoreImage 9 | 10 | public struct Hex { 11 | 12 | let cgColor: CGColor 13 | 14 | /// Default init with CGColor 15 | public init(cgColor: CGColor) { 16 | self.cgColor = cgColor 17 | } 18 | 19 | /// Convenience initializer with hexadecimal values. 20 | public init?(hex: String) { 21 | let hexString = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) 22 | 23 | var hexValue = UInt64() 24 | 25 | guard Scanner(string: hexString).scanHexInt64(&hexValue) else { 26 | return nil 27 | } 28 | 29 | let a, r, g, b: UInt64 30 | switch hexString.count { 31 | case 3: // 0xRGB 32 | (a, r, g, b) = (255, (hexValue >> 8) * 17, (hexValue >> 4 & 0xF) * 17, (hexValue & 0xF) * 17) 33 | case 6: // 0xRRGGBB 34 | (a, r, g, b) = (255, hexValue >> 16, hexValue >> 8 & 0xFF, hexValue & 0xFF) 35 | case 8: // 0xRRGGBBAA 36 | (r, g, b, a) = (hexValue >> 24, hexValue >> 16 & 0xFF, hexValue >> 8 & 0xFF, hexValue & 0xFF) 37 | default: 38 | (a, r, g, b) = (255, 0, 0, 0) 39 | } 40 | 41 | let red = CGFloat(r) / 255 42 | let green = CGFloat(g) / 255 43 | let blue = CGFloat(b) / 255 44 | let alpha = CGFloat(a) / 255 45 | 46 | let components: [CGFloat] = [red, green, blue, alpha] 47 | let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) 48 | 49 | self.cgColor = CGColor(colorSpace: colorSpace!, components: components)! 50 | } 51 | 52 | /// The hexadecimal value of the color. 53 | public var hex: String { 54 | let rgb: Int = (Int)(cgColor.red * 255) << 16 | (Int)(cgColor.green * 255) << 8 | (Int)(cgColor.blue * 255) << 0 55 | return String(format: "#%06x", rgb) 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Sources/DominantColors/Colors/Lab.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Lab.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 04.09.2023. 6 | // 7 | 8 | import CoreImage 9 | 10 | struct Lab { 11 | let L: CGFloat 12 | let a: CGFloat 13 | let b: CGFloat 14 | } 15 | 16 | struct LabCalculator { 17 | static func convert(RGB: RGB) -> Lab { 18 | let XYZ = XYZCalculator.convert(rgb: RGB) 19 | let Lab = LabCalculator.convert(XYZ: XYZ) 20 | return Lab 21 | } 22 | 23 | static let referenceX: CGFloat = 95.047 24 | static let referenceY: CGFloat = 100.0 25 | static let referenceZ: CGFloat = 108.883 26 | 27 | /// http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html 28 | static func convert(XYZ: XYZ) -> Lab { 29 | func transform(value: CGFloat) -> CGFloat { 30 | if value > pow(6/29, 3) { 31 | return pow(value, 1/3) 32 | } else { 33 | return (pow(29/3, 3) * value + 16) / 116 34 | } 35 | } 36 | 37 | let X = transform(value: XYZ.X / referenceX) 38 | let Y = transform(value: XYZ.Y / referenceY) 39 | let Z = transform(value: XYZ.Z / referenceZ) 40 | 41 | let L = ((116.0 * Y) - 16.0).rounded(.toNearestOrAwayFromZero, precision: 10000) 42 | let a = (500.0 * (X - Y)).rounded(.toNearestOrAwayFromZero, precision: 10000) 43 | let b = (200.0 * (Y - Z)).rounded(.toNearestOrAwayFromZero, precision: 10000) 44 | 45 | return Lab(L: L, a: a, b: b) 46 | } 47 | } 48 | 49 | extension CGColor { 50 | 51 | /// The L* value of the CIELAB color space. 52 | /// L* represents the lightness of the color from 0 (black) to 100 (white). 53 | public var L: CGFloat { 54 | let Lab = LabCalculator.convert(RGB: self.rgb) 55 | return Lab.L 56 | } 57 | 58 | /// The a* value of the CIELAB color space. 59 | /// a* represents colors from green to red. 60 | public var a: CGFloat { 61 | let Lab = LabCalculator.convert(RGB: self.rgb) 62 | return Lab.a 63 | } 64 | 65 | /// The b* value of the CIELAB color space. 66 | /// b* represents colors from blue to yellow. 67 | public var b: CGFloat { 68 | let Lab = LabCalculator.convert(RGB: self.rgb) 69 | return Lab.b 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /Sources/DominantColors/Colors/RGB.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RGB.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 04.09.2023. 6 | // 7 | 8 | import CoreImage 9 | 10 | struct RGB: Hashable { 11 | let R: CGFloat 12 | let G: CGFloat 13 | let B: CGFloat 14 | } 15 | 16 | extension CGColor { 17 | 18 | #if os(iOS) 19 | public static var black: CGColor { 20 | CGColor(red: 0, green: 0, blue: 0, alpha: 1) 21 | } 22 | 23 | public static var white: CGColor { 24 | CGColor(red: 1, green: 1, blue: 1, alpha: 1) 25 | } 26 | #endif 27 | 28 | public static var red: CGColor { 29 | CGColor(red: 1, green: 0, blue: 0, alpha: 1) 30 | } 31 | 32 | public static var green: CGColor { 33 | CGColor(red: 0, green: 1, blue: 0, alpha: 1) 34 | } 35 | 36 | public static var blue: CGColor { 37 | CGColor(red: 0, green: 0, blue: 1, alpha: 1) 38 | } 39 | } 40 | 41 | extension CGColor { 42 | // MARK: - Public 43 | 44 | /// The red (R) channel of the RGB color space as a value from 0.0 to 1.0. 45 | public var red: CGFloat { 46 | CIColor(cgColor: self).red 47 | } 48 | 49 | /// The green (G) channel of the RGB color space as a value from 0.0 to 1.0. 50 | public var green: CGFloat { 51 | CIColor(cgColor: self).green 52 | } 53 | 54 | /// The blue (B) channel of the RGB color space as a value from 0.0 to 1.0. 55 | public var blue: CGFloat { 56 | CIColor(cgColor: self).blue 57 | } 58 | 59 | // MARK: Internal 60 | 61 | var red255: CGFloat { 62 | self.red * 255.0 63 | } 64 | 65 | var green255: CGFloat { 66 | self.green * 255.0 67 | } 68 | 69 | var blue255: CGFloat { 70 | self.blue * 255.0 71 | } 72 | 73 | var rgb: RGB { 74 | return RGB(R: self.red, G: self.green, B: self.blue) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/DominantColors/Colors/RGB255.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 26.04.2024. 6 | // 7 | 8 | import CoreGraphics.CGColor 9 | 10 | struct RGB255: Hashable { 11 | let red: UInt8 12 | let green: UInt8 13 | let blue: UInt8 14 | } 15 | 16 | extension RGB255 { 17 | func cgColor(colorSpace: CGColorSpace) -> CGColor { 18 | let red = (CGFloat(red) / 255.0).rounded(.toNearestOrAwayFromZero, precision: 100) 19 | let green = (CGFloat(green) / 255.0).rounded(.toNearestOrAwayFromZero, precision: 100) 20 | let blue = (CGFloat(blue) / 255.0).rounded(.toNearestOrAwayFromZero, precision: 100) 21 | let alpha = 255.0 22 | let components: [CGFloat] = [red, green, blue, alpha] 23 | 24 | return CGColor(colorSpace: colorSpace, components: components) ?? CGColor(srgbRed: red, green: green, blue: blue, alpha: alpha) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/DominantColors/Colors/XYZ.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYZ.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 04.09.2023. 6 | // 7 | 8 | import CoreImage 9 | 10 | struct XYZ { 11 | let X: CGFloat 12 | let Y: CGFloat 13 | let Z: CGFloat 14 | } 15 | 16 | struct XYZCalculator { 17 | 18 | static func convert(rgb: RGB) -> XYZ { 19 | func transform(value: CGFloat) -> CGFloat { 20 | if value > 0.04045 { 21 | return pow((value + 0.055) / 1.055, 2.4) 22 | } else { 23 | return value / 12.92 24 | } 25 | } 26 | 27 | let red = transform(value: rgb.R) * 100.0 28 | let green = transform(value: rgb.G) * 100.0 29 | let blue = transform(value: rgb.B) * 100.0 30 | 31 | let X = (red * 0.4124564 + green * 0.3575761 + blue * 0.1804375).rounded(.toNearestOrAwayFromZero, precision: 10000) 32 | let Y = (red * 0.2126729 + green * 0.7151522 + blue * 0.0721750).rounded(.toNearestOrAwayFromZero, precision: 10000) 33 | let Z = (red * 0.0193339 + green * 0.1191920 + blue * 0.9503041).rounded(.toNearestOrAwayFromZero, precision: 10000) 34 | 35 | return XYZ(X: X, Y: Y, Z: Z) 36 | } 37 | 38 | } 39 | 40 | extension CGColor { 41 | 42 | /// The X value of the XYZ color space. 43 | public var X: CGFloat { 44 | let XYZ = XYZCalculator.convert(rgb: self.rgb) 45 | return XYZ.X 46 | } 47 | 48 | /// The Y value of the XYZ color space. 49 | public var Y: CGFloat { 50 | let XYZ = XYZCalculator.convert(rgb: self.rgb) 51 | return XYZ.Y 52 | } 53 | 54 | /// The Z value of the XYZ color space. 55 | public var Z: CGFloat { 56 | let XYZ = XYZCalculator.convert(rgb: self.rgb) 57 | return XYZ.Z 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /Sources/DominantColors/ComplementaryColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComplementaryColor.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 04.09.2023. 6 | // 7 | 8 | import CoreImage 9 | 10 | extension CGColor { 11 | 12 | /// Computes the complementary color of the current color instance. 13 | /// Complementary colors are opposite on the color wheel. 14 | public var complementaryColor: CGColor { 15 | let red: CGFloat = (255.0 - red255) / 255.0 16 | let green: CGFloat = (255.0 - green255) / 255.0 17 | let blue: CGFloat = (255.0 - blue255) / 255.0 18 | 19 | let components: [CGFloat] = [red, green, blue, alpha] 20 | let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)! 21 | 22 | let cgColor = CGColor(colorSpace: colorSpace, components: components) ?? self 23 | return cgColor 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Sources/DominantColors/ContrastRatio.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContrastRatio.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 04.09.2023. 6 | // 7 | 8 | import CoreImage 9 | 10 | extension CGColor { 11 | 12 | /// An enumeration which groups contrast ratios based on their readability. 13 | /// This follows the Web Content Accessibility Guidelines (WCAG) 2.0. 14 | public enum ContrastRatioResult { 15 | /// The contrast ratio between is enough for most people to distinguish the two colors. 16 | /// It can be used as text / background. 17 | case acceptable(CGFloat) 18 | 19 | /// The contrast ratio is not big enough for most people to distinguish the two colors. 20 | /// It should only be used for large text / background. 21 | case acceptableForLargeText(CGFloat) 22 | 23 | /// The contrast ratio between the two colors is low. 24 | /// It will be difficult for most to distinguish the two colors easily. 25 | /// Do not use these two colors as text / background. 26 | case low(CGFloat) 27 | 28 | init(value: CGFloat) { 29 | if value >= 4.5 { 30 | self = .acceptable(value) 31 | } else if value >= 3.0 { 32 | self = .acceptableForLargeText(value) 33 | } else { 34 | self = .low(value) 35 | } 36 | } 37 | 38 | var associatedValue: CGFloat { 39 | switch self { 40 | case .acceptable(let value), 41 | .acceptableForLargeText(let value), 42 | .low(let value): 43 | return value 44 | } 45 | } 46 | } 47 | 48 | /// Computes the contrast ratio between the current color instance, and the one passed in. 49 | /// Contrast ratios can range from 1 to 21 (commonly written 1:1 to 21:1). 50 | public func contrastRatio(with color: CGColor) -> ContrastRatioResult { 51 | let l1 = max(color.relativeLuminance, relativeLuminance) 52 | let l2 = min(color.relativeLuminance, relativeLuminance) 53 | 54 | let contrastRatio = (l1 + 0.05) / (l2 + 0.05) 55 | return ContrastRatioResult(value: contrastRatio.rounded(.toNearestOrAwayFromZero, precision: 100)) 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Sources/DominantColors/DeltaEFormula.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 04.09.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// The different algorithms for comparing colors. 11 | /// @see https://en.wikipedia.org/wiki/Color_difference 12 | public enum DeltaEFormula: Int, CaseIterable, Identifiable { 13 | /// The euclidean algorithm is the simplest and fastest one, but will yield results that are unexpected to the human eye. Especially in the green range. 14 | /// It simply calculates the euclidean distance in the RGB color space. 15 | case euclidean 16 | 17 | /// The `CIE76`algorithm is fast and yields acceptable results in most scenario. 18 | case CIE76 19 | 20 | /// The `CIE94` algorithm is an improvement to the `CIE76`, especially for the saturated regions. It's marginally slower than `CIE76`. 21 | case CIE94 22 | 23 | /// The `CIEDE2000` algorithm is the most precise algorithm to compare colors. 24 | /// It is considerably slower than its predecessors. 25 | case CIEDE2000 26 | 27 | /// The `CMC` algorithm is defined a difference measure, based on the L*C*h color model. 28 | /// The quasimetric has two parameters: lightness (l) and chroma (c), allowing the users to weight the difference based on the ratio of l:c that is deemed appropriate for the application. 29 | case CMC 30 | 31 | public var method: String { 32 | switch self { 33 | case .euclidean: 34 | "Euclidean" 35 | case .CIE76: 36 | "CIE76" 37 | case .CIE94: 38 | "CIE94" 39 | case .CIEDE2000: 40 | "CIEDE2000" 41 | case .CMC: 42 | "CMC" 43 | } 44 | } 45 | 46 | public var id: String { 47 | self.method 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/DominantColors/DominantColorAlgorithm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 04.09.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum DominantColorAlgorithm: Identifiable, Hashable { 11 | 12 | /// Finds the dominant colors of an image by iterating, grouping and sorting its pixels. 13 | case iterative(formula: DeltaEFormula) 14 | 15 | /// Finds the dominant colors of an image by using using a k-means clustering algorithm. 16 | case kMeansClustering 17 | 18 | /// Finds the dominant colors of an image by using using a area average algorithm. 19 | case areaAverage 20 | 21 | var algorithm: String { 22 | switch self { 23 | case .iterative: 24 | "Iterative" 25 | case .kMeansClustering: 26 | "K-means Clustering" 27 | case .areaAverage: 28 | "Area Average" 29 | } 30 | } 31 | 32 | public var id: String { 33 | self.algorithm 34 | } 35 | 36 | public func hash(into hasher: inout Hasher) { 37 | hasher.combine(self.algorithm) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/DominantColors/DominantColorQuality.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DominantColorQuality.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 04.09.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Represents how precise the dominant color algorithm should be. 11 | /// The lower the quality, the faster the algorithm. 12 | /// `.best` should only be reserved for very small images. 13 | public enum DominantColorQuality: String, Codable, CaseIterable, CustomStringConvertible { 14 | case low 15 | case fair 16 | case high 17 | case best 18 | 19 | var prefferedImageArea: CGFloat? { 20 | switch self { 21 | case .low: 22 | return 1_000 23 | case .fair: 24 | return 10_000 25 | case .high: 26 | return 100_000 27 | case .best: 28 | return nil 29 | } 30 | } 31 | 32 | var kMeansInputPasses: Int { 33 | switch self { 34 | case .low: 35 | return 1 36 | case .fair: 37 | return 10 38 | case .high: 39 | return 15 40 | case .best: 41 | return 20 42 | } 43 | } 44 | 45 | /// NSNumber for attribute type for CIPixellate filter pixel size. 46 | var pixellateScale: NSNumber { 47 | switch self { 48 | case .low: 49 | return 64 50 | case .fair: 51 | return 32 52 | case .high: 53 | return 16 54 | case .best: 55 | return 2 56 | } 57 | } 58 | 59 | /// Returns a new size (with the same aspect ratio) that takes into account the quality to match. 60 | /// For example with a `.low` quality, the returned size will be much smaller. 61 | /// On the opposite, with a `.best` quality, the returned size will be identical to the original size. 62 | func targetSize(for originalSize: CGSize) -> CGSize { 63 | guard let prefferedImageArea = prefferedImageArea else { 64 | return originalSize 65 | } 66 | 67 | let originalArea = originalSize.area 68 | 69 | guard originalArea > prefferedImageArea else { 70 | return originalSize 71 | } 72 | 73 | return originalSize.transformToFit(in: prefferedImageArea) 74 | } 75 | 76 | public var description: String { 77 | self.rawValue.capitalized 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/DominantColors/DominantColors+CIAreaAverage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DominantColors+CIAreaAverage.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 01.05.2024. 6 | // 7 | 8 | import Foundation 9 | import CoreImage 10 | 11 | extension DominantColors { 12 | /// Extract the dominant colors of the image. 13 | /// 14 | /// Finds the dominant colors of an image by using using a area average algorithm with CIAreaAverage filter. 15 | /// - Parameters: 16 | /// - image: Source image for extract colors. 17 | /// - count: Number of colors for the image. 18 | /// - sorting: Type of sorting sequence colors. 19 | /// - Returns: Average colors, specified as an array of `CGColor` instances. 20 | public static func averageColors( 21 | image: CGImage, 22 | count: Int = 8, 23 | sorting: Sort = .frequency 24 | ) throws -> [CGColor] { 25 | let averageColors = try areaAverageColors(image: image, count: UInt8(count), sorting: sorting) 26 | return averageColors 27 | } 28 | 29 | static func areaAverageColors( 30 | image: CGImage, 31 | count: UInt8, 32 | sorting: Sort = .frequency 33 | ) throws -> [CGColor] { 34 | let ciImage = CIImage(cgImage: image) 35 | guard 36 | 1...image.width ~= Int(count), 37 | count != .zero 38 | else { 39 | throw ImageColorError.lowResolutionFailure 40 | } 41 | let extentVectors = [Int] (0...(Int(count)-1)).map { part in 42 | let partWidth = ciImage.extent.size.width / CGFloat(count) 43 | let extentVector = CIVector( 44 | x: partWidth * CGFloat(part), 45 | y: ciImage.extent.origin.y, 46 | z: partWidth, 47 | w: ciImage.extent.size.height 48 | ) 49 | return extentVector 50 | } 51 | 52 | let filters = extentVectors.compactMap { 53 | let filter = CIFilter( 54 | name: "CIAreaAverage", 55 | parameters: [kCIInputImageKey: ciImage, kCIInputExtentKey: $0] 56 | ) 57 | return filter 58 | } 59 | let outputImages = filters.compactMap { $0.outputImage } 60 | 61 | var bitmaps: [[UInt8]] = [] 62 | 63 | guard let kCFNull = kCFNull else { 64 | throw ImageColorError.kCFNullFailure 65 | } 66 | 67 | let context = CIContext(options: [.workingColorSpace: kCFNull]) 68 | outputImages.forEach { outputImage in 69 | var bitmap = [UInt8](repeating: 0, count: 4) 70 | context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil) 71 | bitmaps.append(bitmap) 72 | } 73 | 74 | var averageColors = bitmaps.compactMap { bitmap -> CGColor? in 75 | let red = CGFloat(bitmap[0]) / 255.0 76 | let green = CGFloat(bitmap[1]) / 255.0 77 | let blue = CGFloat(bitmap[2]) / 255.0 78 | let alpha = CGFloat(bitmap[3]) / 255.0 79 | 80 | let components: [CGFloat] = [red, green, blue, alpha] 81 | guard 82 | let colorSpace = image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB), 83 | let cgColor = CGColor(colorSpace: colorSpace, components: components) 84 | else { return nil } 85 | 86 | return cgColor 87 | } 88 | 89 | switch sorting { 90 | case .darkness: 91 | averageColors = averageColors.sorted(by: { (lhs, rhs) -> Bool in 92 | lhs.lightness < rhs.lightness 93 | }) 94 | case .lightness: 95 | averageColors = averageColors.sorted(by: { (lhs, rhs) -> Bool in 96 | lhs.lightness > rhs.lightness 97 | }) 98 | case .frequency: 99 | break 100 | } 101 | 102 | return averageColors 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/DominantColors/DominantColors+CIKMeans.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DominantColors+CIKMeans.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 01.05.2024. 6 | // 7 | 8 | import Foundation 9 | import CoreImage 10 | 11 | extension DominantColors { 12 | /// Extract the dominant colors of the image. 13 | /// 14 | /// Finds the dominant colors of an image by using using a k-means clustering algorithm. 15 | /// - Parameters: 16 | /// - image: Source image for extract colors. 17 | /// - count: Number of colors for the image. 18 | /// - sorting: Type of sorting sequence colors. 19 | /// - Returns: Cluster average colors as an array of `CGColor` instances. 20 | public static func kMeansClusteringColors( 21 | image: CGImage, 22 | quality: DominantColorQuality = .fair, 23 | count: Int = 8, 24 | sorting: Sort = .frequency 25 | ) throws -> [CGColor] { 26 | let kMeansClusteringColors = try kMeansClustering(image: image, with: quality, count: count, sorting: sorting) 27 | return kMeansClusteringColors 28 | } 29 | 30 | static func kMeansClustering( 31 | image: CGImage, 32 | with quality: DominantColorQuality, 33 | count: Int, 34 | sorting: Sort = .frequency 35 | ) throws -> [CGColor] { 36 | guard count > 0 else { return [] } 37 | let ciImage = CIImage(cgImage: image) 38 | let filter = "CIKMeans" 39 | guard let kMeansFilter = CIFilter(name: filter) else { 40 | throw ImageColorError.ciFilterCreateFailure(filter: filter) 41 | } 42 | 43 | let clusterCount = count 44 | 45 | kMeansFilter.setValue(ciImage, forKey: kCIInputImageKey) 46 | kMeansFilter.setValue(CIVector(cgRect: ciImage.extent), forKey: "inputExtent") 47 | kMeansFilter.setValue(clusterCount, forKey: "inputCount") 48 | kMeansFilter.setValue(quality.kMeansInputPasses, forKey: "inputPasses") 49 | kMeansFilter.setValue(NSNumber(value: true), forKey: "inputPerceptual") 50 | 51 | guard var outputImage = kMeansFilter.outputImage else { 52 | throw ImageColorError.outputImageFailure 53 | } 54 | 55 | outputImage = outputImage.settingAlphaOne(in: outputImage.extent) 56 | 57 | let context = CIContext() 58 | var bitmap = [UInt8](repeating: 0, count: 4 * clusterCount) 59 | 60 | context.render(outputImage, toBitmap: &bitmap, rowBytes: 4 * clusterCount, bounds: outputImage.extent, format: CIFormat.RGBA8, colorSpace: ciImage.colorSpace!) 61 | 62 | var dominantColors = [CGColor]() 63 | 64 | for i in 0.. Bool in 83 | lhs.lightness < rhs.lightness 84 | }) 85 | case .lightness: 86 | dominantColors = dominantColors.sorted(by: { (lhs, rhs) -> Bool in 87 | lhs.lightness > rhs.lightness 88 | }) 89 | case .frequency: 90 | break 91 | } 92 | 93 | return dominantColors 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/DominantColors/DominantColors+ColorResolved.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DominantColors+ColorResolved.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 11.05.2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) 11 | extension DominantColors { 12 | 13 | /// Extract the dominant colors of the image. 14 | /// 15 | /// All colors are combined together based on similarity. 16 | /// This avoids having to deal with many shades of the same colors, which often happens when dealing with compression artifacts (jpegs, etc.). 17 | /// - Parameters: 18 | /// - image: Source image for extract colors. 19 | /// - quality: The quality used to determine the dominant colors. A higher quality will yield more accurate results, but will be slower. 20 | /// - algorithm: The different algorithms for comparing colors. 21 | /// - maxCount: Maximum number of colors for the image. 22 | /// - options: Some of additional options for removing flowers. 23 | /// - sorting: Type of sorting sequence colors. 24 | /// - deltaColors: The score that needs to be met to consider two colors similar. The larger the value, the fewer shades will be obtained from the images 25 | /// - 10 by default to match similar shades 26 | /// - 2.3 approximately corresponds to the minimum difference between colors visible to the human eye. 27 | /// - resultLog: Prints a transcript of the results with the total time and number of colors to the console for debug. 28 | /// - timeLog: Prints each step of the algorithm with time to the console for debug. 29 | /// - Returns: The dominant colors as array of `Color.Resolved` instances. 30 | public static func dominantColorsResolved( 31 | image: CGImage, 32 | quality: DominantColorQuality = .fair, 33 | algorithm: DeltaEFormula = .CIE94, 34 | maxCount: Int = 8, 35 | options: [Options] = [], 36 | sorting: Sort = .frequency, 37 | deltaColors: CGFloat = 10, 38 | resultLog: Bool = false, 39 | timeLog: Bool = false 40 | ) throws -> [Color.Resolved] { 41 | let colorFrequencies = try Self.dominantColorFrequencies( 42 | image: image, 43 | quality: quality, 44 | formula: algorithm, 45 | maxCount: maxCount, 46 | options: options, 47 | sorting: sorting, 48 | deltaColors: deltaColors, 49 | resultLog: resultLog, 50 | timeLog: timeLog 51 | ) 52 | let dominantColors = colorFrequencies.map { cgColor2Resolved(cgColor: $0.color)} 53 | return dominantColors 54 | } 55 | 56 | /// Extract the dominant colors of the image. 57 | /// 58 | /// Finds the dominant colors of an image by using using a area average algorithm with CIAreaAverage filter. 59 | /// - Parameters: 60 | /// - image: Source image for extract colors. 61 | /// - count: Number of colors for the image. 62 | /// - sorting: Type of sorting sequence colors. 63 | /// - Returns: Average colors, specified as an array of `Color.Resolved` instances. 64 | public static func averageColorsResolved( 65 | image: CGImage, 66 | count: Int = 8, 67 | sorting: Sort = .frequency 68 | ) throws -> [Color.Resolved] { 69 | let averageColors = try areaAverageColors(image: image, count: UInt8(count), sorting: sorting) 70 | .map { cgColor2Resolved(cgColor: $0)} 71 | return averageColors 72 | } 73 | 74 | /// Extract the dominant colors of the image. 75 | /// 76 | /// Finds the dominant colors of an image by using using a k-means clustering algorithm. 77 | /// - Parameters: 78 | /// - image: Source image for extract colors. 79 | /// - count: Number of colors for the image. 80 | /// - sorting: Type of sorting sequence colors. 81 | /// - Returns: Cluster average colors as an array of `Color.Resolved` instances. 82 | public static func kMeansClusteringColorsResolved( 83 | image: CGImage, 84 | quality: DominantColorQuality = .fair, 85 | count: Int = 8, 86 | sorting: Sort = .frequency 87 | ) throws -> [Color.Resolved] { 88 | let clusteringColors = try kMeansClustering(image: image, with: quality, count: count, sorting: sorting) 89 | .map { cgColor2Resolved(cgColor: $0)} 90 | return clusteringColors 91 | } 92 | 93 | private static func cgColor2Resolved(cgColor: CGColor) -> Color.Resolved { 94 | Color.Resolved( 95 | colorSpace: .sRGB, 96 | red: Float(cgColor.red), 97 | green: Float(cgColor.green), 98 | blue: Float(cgColor.blue), 99 | opacity: Float(cgColor.alpha)) 100 | } 101 | } 102 | 103 | -------------------------------------------------------------------------------- /Sources/DominantColors/DominantColors+NSColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DominantColors+NSColor.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 11.05.2024. 6 | // 7 | 8 | #if os(OSX) 9 | import AppKit 10 | 11 | @available(macOS 10.8, *) 12 | extension DominantColors { 13 | 14 | /// Extract the dominant colors of the image. 15 | /// 16 | /// All colors are combined together based on similarity. 17 | /// This avoids having to deal with many shades of the same colors, which often happens when dealing with compression artifacts (jpegs, etc.). 18 | /// - Parameters: 19 | /// - image: Source image for extract colors. 20 | /// - quality: The quality used to determine the dominant colors. A higher quality will yield more accurate results, but will be slower. 21 | /// - algorithm: The different algorithms for comparing colors. 22 | /// - maxCount: Maximum number of colors for the image. 23 | /// - options: Some of additional options for removing flowers. 24 | /// - sorting: Type of sorting sequence colors. 25 | /// - deltaColors: The score that needs to be met to consider two colors similar. The larger the value, the fewer shades will be obtained from the images 26 | /// - 10 by default to match similar shades 27 | /// - 2.3 approximately corresponds to the minimum difference between colors visible to the human eye. 28 | /// - resultLog: Prints a transcript of the results with the total time and number of colors to the console for debug. 29 | /// - timeLog: Prints each step of the algorithm with time to the console for debug. 30 | /// - Returns: The dominant colors as array of `Color.Resolved` instances. 31 | public static func dominantColors( 32 | nsImage: NSImage, 33 | quality: DominantColorQuality = .fair, 34 | algorithm: DeltaEFormula = .CIE94, 35 | maxCount: Int = 8, 36 | options: [Options] = [], 37 | sorting: Sort = .frequency, 38 | deltaColors: CGFloat = 10, 39 | resultLog: Bool = false, 40 | timeLog: Bool = false 41 | ) throws -> [NSColor] { 42 | let cgImage = try cgImage(from: nsImage) 43 | let colorFrequencies = try dominantColorFrequencies( 44 | image: cgImage, 45 | quality: quality, 46 | formula: algorithm, 47 | maxCount: maxCount, 48 | options: options, 49 | sorting: sorting, 50 | deltaColors: deltaColors, 51 | resultLog: resultLog, 52 | timeLog: timeLog 53 | ) 54 | let dominantColors = colorFrequencies.compactMap { NSColor(cgColor: $0.color) } 55 | return dominantColors 56 | } 57 | 58 | /// Extract the dominant colors of the image. 59 | /// 60 | /// Finds the dominant colors of an image by using using a area average algorithm with CIAreaAverage filter. 61 | /// - Parameters: 62 | /// - image: Source image for extract colors. 63 | /// - count: Number of colors for the image. 64 | /// - sorting: Type of sorting sequence colors. 65 | /// - Returns: Average colors, specified as an array of `Color.Resolved` instances. 66 | public static func averageColors( 67 | nsImage: NSImage, 68 | count: Int = 8, 69 | sorting: Sort = .frequency 70 | ) throws -> [NSColor] { 71 | let cgImage = try cgImage(from: nsImage) 72 | let averageColors = try areaAverageColors(image: cgImage, count: UInt8(count), sorting: sorting) 73 | .compactMap { NSColor(cgColor: $0) } 74 | return averageColors 75 | } 76 | 77 | /// Extract the dominant colors of the image. 78 | /// 79 | /// Finds the dominant colors of an image by using using a k-means clustering algorithm. 80 | /// - Parameters: 81 | /// - image: Source image for extract colors. 82 | /// - count: Number of colors for the image. 83 | /// - sorting: Type of sorting sequence colors. 84 | /// - Returns: Cluster average colors as an array of `Color.Resolved` instances. 85 | public static func kMeansClusteringColors( 86 | nsImage: NSImage, 87 | quality: DominantColorQuality = .fair, 88 | count: Int = 8, 89 | sorting: Sort = .frequency 90 | ) throws -> [NSColor] { 91 | let cgImage = try cgImage(from: nsImage) 92 | let kMeansClusteringColors = try kMeansClustering(image: cgImage, with: quality, count: count, sorting: sorting) 93 | .compactMap { NSColor(cgColor: $0) } 94 | return kMeansClusteringColors 95 | } 96 | 97 | private static func cgImage(from nsImage: NSImage) throws -> CGImage { 98 | guard let cgImage = nsImage.cgImage(forProposedRect: nil, context: nil, hints: nil) else { throw ImageColorError.cgImageFailure } 99 | return cgImage 100 | } 101 | } 102 | 103 | #endif 104 | -------------------------------------------------------------------------------- /Sources/DominantColors/DominantColors+UIColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DominantColors+UIColor.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 11.05.2024. 6 | // 7 | 8 | #if os(iOS) 9 | import UIKit 10 | 11 | @available(iOS 2.0, *) 12 | extension DominantColors { 13 | 14 | /// Extract the dominant colors of the image. 15 | /// 16 | /// All colors are combined together based on similarity. 17 | /// This avoids having to deal with many shades of the same colors, which often happens when dealing with compression artifacts (jpegs, etc.). 18 | /// - Parameters: 19 | /// - image: Source image for extract colors. 20 | /// - quality: The quality used to determine the dominant colors. A higher quality will yield more accurate results, but will be slower. 21 | /// - algorithm: The different algorithms for comparing colors. 22 | /// - maxCount: Maximum number of colors for the image. 23 | /// - options: Some of additional options for removing flowers. 24 | /// - sorting: Type of sorting sequence colors. 25 | /// - deltaColors: The score that needs to be met to consider two colors similar. The larger the value, the fewer shades will be obtained from the images 26 | /// - 10 by default to match similar shades 27 | /// - 2.3 approximately corresponds to the minimum difference between colors visible to the human eye. 28 | /// - resultLog: Prints a transcript of the results with the total time and number of colors to the console for debug. 29 | /// - timeLog: Prints each step of the algorithm with time to the console for debug. 30 | /// - Returns: The dominant colors as array of `Color.Resolved` instances. 31 | public static func dominantColors( 32 | uiImage: UIImage, 33 | quality: DominantColorQuality = .fair, 34 | algorithm: DeltaEFormula = .CIE94, 35 | maxCount: Int = 8, 36 | options: [Options] = [], 37 | sorting: Sort = .frequency, 38 | deltaColors: CGFloat = 10, 39 | resultLog: Bool = false, 40 | timeLog: Bool = false 41 | ) throws -> [UIColor] { 42 | let cgImage = try cgImage(from: uiImage) 43 | let colorFrequencies = try dominantColorFrequencies( 44 | image: cgImage, 45 | quality: quality, 46 | formula: algorithm, 47 | maxCount: maxCount, 48 | options: options, 49 | sorting: sorting, 50 | deltaColors: deltaColors, 51 | resultLog: resultLog, 52 | timeLog: timeLog 53 | ) 54 | let dominantColors = colorFrequencies.map { UIColor(cgColor: $0.color) } 55 | return dominantColors 56 | } 57 | 58 | /// Extract the dominant colors of the image. 59 | /// 60 | /// Finds the dominant colors of an image by using using a area average algorithm with CIAreaAverage filter. 61 | /// - Parameters: 62 | /// - image: Source image for extract colors. 63 | /// - count: Number of colors for the image. 64 | /// - sorting: Type of sorting sequence colors. 65 | /// - Returns: Average colors, specified as an array of `Color.Resolved` instances. 66 | public static func averageColors( 67 | uiImage: UIImage, 68 | count: Int = 8, 69 | sorting: Sort = .frequency 70 | ) throws -> [UIColor] { 71 | let cgImage = try cgImage(from: uiImage) 72 | let averageColors = try areaAverageColors(image: cgImage, count: UInt8(count), sorting: sorting) 73 | .map { UIColor(cgColor: $0) } 74 | return averageColors 75 | } 76 | 77 | /// Extract the dominant colors of the image. 78 | /// 79 | /// Finds the dominant colors of an image by using using a k-means clustering algorithm. 80 | /// - Parameters: 81 | /// - image: Source image for extract colors. 82 | /// - count: Number of colors for the image. 83 | /// - sorting: Type of sorting sequence colors. 84 | /// - Returns: Cluster average colors as an array of `Color.Resolved` instances. 85 | public static func kMeansClusteringColors( 86 | uiImage: UIImage, 87 | quality: DominantColorQuality = .fair, 88 | count: Int = 8, 89 | sorting: Sort = .frequency 90 | ) throws -> [UIColor] { 91 | let cgImage = try cgImage(from: uiImage) 92 | let kMeansClusteringColors = try kMeansClustering(image: cgImage, with: quality, count: count, sorting: sorting) 93 | .map { UIColor(cgColor: $0) } 94 | return kMeansClusteringColors 95 | } 96 | 97 | private static func cgImage(from uiImage: UIImage) throws -> CGImage { 98 | guard let cgImage = uiImage.cgImage else { throw ImageColorError.cgImageFailure } 99 | return cgImage 100 | } 101 | } 102 | #endif 103 | -------------------------------------------------------------------------------- /Sources/DominantColors/DominantColors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DominantColors.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 03.09.2023. 6 | // 7 | 8 | import CoreImage 9 | 10 | public class DominantColors { 11 | /// Enum of additional options for removing flowers. 12 | public enum Options { 13 | /// Remove pure black colors 14 | case excludeBlack 15 | /// Remove pure white colors 16 | case excludeWhite 17 | /// Remove pure gray colors 18 | case excludeGray 19 | } 20 | 21 | /// Color sorting options. 22 | public enum Sort: CaseIterable, Identifiable { 23 | /// Sorting colors from darkness to lightness. 24 | case darkness 25 | /// Sorting colors from lightness to darkness. 26 | case lightness 27 | /// Sorting colors by frequency in image. 28 | case frequency 29 | 30 | public var name: String { 31 | switch self { 32 | case .darkness: 33 | "Darkness" 34 | case .lightness: 35 | "Lightness" 36 | case .frequency: 37 | "Frequency" 38 | } 39 | } 40 | 41 | public var id: String { 42 | self.name 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/DominantColors/Extensions/CGFloatExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 04.09.2023. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | extension CGFloat { 11 | 12 | func rounded(_ rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero, precision: Int) -> CGFloat { 13 | return (self * CGFloat(precision)).rounded(rule) / CGFloat(precision) 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Sources/DominantColors/Extensions/CGImageExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 03.09.2023. 6 | // 7 | 8 | import CoreImage 9 | 10 | extension CGImage { 11 | 12 | /// Get the dominant colors of the image. 13 | /// 14 | /// Finds the primary colors in an image using color difference formulas. 15 | /// This avoids having to deal with many shades of the same colors, which often happens when dealing with compression artifacts (jpegs, etc.). 16 | /// - Parameters: 17 | /// - count: the maximum number of colors to extract for the image. 18 | /// - options: additional options for extracted colors.. 19 | /// - Returns: colors as an array of `CGColor` instances. 20 | public func dominantColors(max count: Int = 8, options: [DominantColors.Options] = []) throws -> [CGColor] { 21 | try DominantColors.dominantColors(image: self, maxCount: count, options: options) 22 | } 23 | 24 | /// Initializes contrast colors from the passed colors. 25 | /// Colors should be sorted by importance, with the first color being the most important. 26 | /// This makes it easy to create palettes from a collection of colors. 27 | /// 28 | /// - Parameters: 29 | /// - darkBackground: Whether the color palette should have a dark background. If set to false, the background can be dark or bright. 30 | /// - ignoreContrastRatio: Whether the color palette should ignore the contrast ratio between different colors. It is recommended to set this value to false (the default) if the color palette will be used to display text. 31 | public func contrastColors(darkBackground: Bool = true, ignoreContrastRatio: Bool = false) -> ContrastColors? { 32 | guard let dominantColors = try? self.dominantColors() else { return nil } 33 | let contrastColors = ContrastColors(orderedColors: dominantColors, darkBackground: darkBackground, ignoreContrastRatio: ignoreContrastRatio) 34 | return contrastColors 35 | } 36 | 37 | var resolution: CGSize { 38 | return CGSize(width: width, height: height) 39 | } 40 | 41 | func resize(to targetSize: CGSize) -> CGImage { 42 | guard targetSize != resolution else { 43 | return self 44 | } 45 | 46 | var ratio: Double = 0.0 47 | 48 | // Get ratio (landscape or portrait) 49 | if width > height { 50 | ratio = targetSize.width / CGFloat(width) 51 | } else { 52 | ratio = targetSize.height / CGFloat(height) 53 | } 54 | 55 | let ciImage = CIImage(cgImage: self) 56 | 57 | let filter = CIFilter(name: "CILanczosScaleTransform")! 58 | filter.setValue(ciImage, forKey: "inputImage") 59 | filter.setValue(ratio, forKey: "inputScale") 60 | filter.setValue(1.0, forKey: "inputAspectRatio") 61 | guard let outputImage = filter.value(forKey: "outputImage") as? CIImage else { return self } 62 | 63 | let context = CIContext(options: [.useSoftwareRenderer: false]) 64 | guard let resizedImage = context.createCGImage(outputImage, from: outputImage.extent) else { return self } 65 | 66 | return resizedImage 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /Sources/DominantColors/Extensions/CGSizeExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 03.09.2023. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | extension CGSize { 11 | 12 | /// The area of the size. 13 | var area: CGFloat { 14 | return width * height 15 | } 16 | 17 | /// Returns a new size of the target area, keeping the same aspect ratio. 18 | func transformToFit(in targetArea: CGFloat) -> CGSize { 19 | let ratio = area / targetArea 20 | let targetSize = CGSize(width: width / sqrt(ratio), height: height / sqrt(ratio)) 21 | 22 | return targetSize 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/DominantColors/Extensions/NSImageExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSImageExtension.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 12.05.2024. 6 | // 7 | 8 | #if os(OSX) 9 | import AppKit 10 | 11 | extension NSImage { 12 | /// Get the dominant colors of the image. 13 | /// 14 | /// Finds the primary colors in an image using color difference formulas. 15 | /// This avoids having to deal with many shades of the same colors, which often happens when dealing with compression artifacts (jpegs, etc.). 16 | /// - Parameters: 17 | /// - count: the maximum number of colors to extract for the image. 18 | /// - options: additional options for extracted colors.. 19 | /// - Returns: colors as an array of `NSColor` instances. 20 | public func dominantColors(max count: Int = 8, options: [DominantColors.Options] = []) throws -> [NSColor] { 21 | try DominantColors.dominantColors(nsImage: self, maxCount: count, options: options) 22 | } 23 | 24 | /// Initializes contrast colors from the passed colors. 25 | /// Colors should be sorted by importance, with the first color being the most important. 26 | /// This makes it easy to create palettes from a collection of colors. 27 | /// 28 | /// - Parameters: 29 | /// - darkBackground: Whether the color palette should have a dark background. If set to false, the background can be dark or bright. 30 | /// - ignoreContrastRatio: Whether the color palette should ignore the contrast ratio between different colors. It is recommended to set this value to false (the default) if the color palette will be used to display text. 31 | public func contrastColors(darkBackground: Bool = true, ignoreContrastRatio: Bool = false) -> ContrastColors? { 32 | guard let dominantColors = try? self.dominantColors() else { return nil } 33 | let contrastColors = ContrastColors(orderedColors: dominantColors.map({ $0.cgColor }), darkBackground: darkBackground, ignoreContrastRatio: ignoreContrastRatio) 34 | return contrastColors 35 | } 36 | } 37 | #endif 38 | -------------------------------------------------------------------------------- /Sources/DominantColors/Extensions/UIImageExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageExtension.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 12.05.2024. 6 | // 7 | 8 | #if os(iOS) 9 | import UIKit 10 | 11 | extension UIImage { 12 | /// Get the dominant colors of the image. 13 | /// 14 | /// Finds the primary colors in an image using color difference formulas. 15 | /// This avoids having to deal with many shades of the same colors, which often happens when dealing with compression artifacts (jpegs, etc.). 16 | /// - Parameters: 17 | /// - count: the maximum number of colors to extract for the image. 18 | /// - options: additional options for extracted colors.. 19 | /// - Returns: colors as an array of `UIColor` instances. 20 | public func dominantColors(max count: Int = 8, options: [DominantColors.Options] = []) throws -> [UIColor] { 21 | try DominantColors.dominantColors(uiImage: self, maxCount: count, options: options) 22 | } 23 | 24 | /// Initializes contrast colors from the passed colors. 25 | /// Colors should be sorted by importance, with the first color being the most important. 26 | /// This makes it easy to create palettes from a collection of colors. 27 | /// 28 | /// - Parameters: 29 | /// - darkBackground: Whether the color palette should have a dark background. If set to false, the background can be dark or bright. 30 | /// - ignoreContrastRatio: Whether the color palette should ignore the contrast ratio between different colors. It is recommended to set this value to false (the default) if the color palette will be used to display text. 31 | public func contrastColors(darkBackground: Bool = true, ignoreContrastRatio: Bool = false) -> ContrastColors? { 32 | guard let dominantColors = try? self.dominantColors() else { return nil } 33 | let contrastColors = ContrastColors(orderedColors: dominantColors.map({ $0.cgColor }), darkBackground: darkBackground, ignoreContrastRatio: ignoreContrastRatio) 34 | return contrastColors 35 | } 36 | } 37 | #endif 38 | -------------------------------------------------------------------------------- /Sources/DominantColors/GradientColors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientColors.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 03.12.2023. 6 | // https://stackoverflow.com/questions/15032562/ios-find-color-at-point-between-two-colors 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | #endif 12 | 13 | import CoreGraphics.CGColor 14 | 15 | extension Array where Element == CGColor { 16 | public func gradientColor(percent: CGFloat) -> CGColor { 17 | let percentage: CGFloat = Swift.max(Swift.min(percent, 100), 0) / 100 18 | switch percentage { 19 | case 0: 20 | return first ?? .clear 21 | case 1: 22 | return last ?? .clear 23 | default: 24 | let approxIndex = percentage / (1 / CGFloat(count - 1)) 25 | let firstIndex = Int(approxIndex.rounded(.down)) 26 | let secondIndex = Int(approxIndex.rounded(.up)) 27 | 28 | let firstColor = self[firstIndex] 29 | let secondColor = self[secondIndex] 30 | 31 | let (red1, green1, blue1, alpha1): (CGFloat, CGFloat, CGFloat, CGFloat) = ( 32 | firstColor.components?[0] ?? .zero, 33 | firstColor.components?[1] ?? .zero, 34 | firstColor.components?[2] ?? .zero, 35 | firstColor.components?[3] ?? .zero 36 | ) 37 | let (red2, green2, blue2, alpha2): (CGFloat, CGFloat, CGFloat, CGFloat) = ( 38 | secondColor.components?[0] ?? .zero, 39 | secondColor.components?[1] ?? .zero, 40 | secondColor.components?[2] ?? .zero, 41 | secondColor.components?[3] ?? .zero 42 | ) 43 | 44 | let intermediatePercentage = approxIndex - CGFloat(firstIndex) 45 | 46 | let cgColor = CGColor( 47 | red: CGFloat(red1 + (red2 - red1) * intermediatePercentage), 48 | green: CGFloat(green1 + (green2 - green1) * intermediatePercentage), 49 | blue: CGFloat(blue1 + (blue2 - blue1) * intermediatePercentage), 50 | alpha: CGFloat(alpha1 + (alpha2 - alpha1) * intermediatePercentage) 51 | ) 52 | 53 | return cgColor 54 | } 55 | } 56 | 57 | public func gradientColor(at point: CGFloat, size: CGFloat) -> CGColor { 58 | guard point <= size, 59 | point >= 1, 60 | size >= 2, 61 | count >= 2 62 | else { return .clear } 63 | 64 | switch point { 65 | case 1: 66 | return first ?? .clear 67 | case size: 68 | return last ?? .clear 69 | default: 70 | let percentage: CGFloat = (point - 1) / (size - 1) 71 | let approxIndex = percentage / (1 / CGFloat(count - 1)) 72 | let firstIndex = Int(approxIndex.rounded(.down)) 73 | let secondIndex = Int(approxIndex.rounded(.up)) 74 | 75 | let firstColor = self[firstIndex] 76 | let secondColor = self[secondIndex] 77 | 78 | let (red1, green1, blue1, alpha1): (CGFloat, CGFloat, CGFloat, CGFloat) = ( 79 | firstColor.components?[0] ?? .zero, 80 | firstColor.components?[1] ?? .zero, 81 | firstColor.components?[2] ?? .zero, 82 | firstColor.components?[3] ?? .zero 83 | ) 84 | let (red2, green2, blue2, alpha2): (CGFloat, CGFloat, CGFloat, CGFloat) = ( 85 | secondColor.components?[0] ?? .zero, 86 | secondColor.components?[1] ?? .zero, 87 | secondColor.components?[2] ?? .zero, 88 | secondColor.components?[3] ?? .zero 89 | ) 90 | 91 | let intermediatePercentage = approxIndex - CGFloat(firstIndex) 92 | 93 | let cgColor = CGColor( 94 | red: CGFloat(red1 + (red2 - red1) * intermediatePercentage), 95 | green: CGFloat(green1 + (green2 - green1) * intermediatePercentage), 96 | blue: CGFloat(blue1 + (blue2 - blue1) * intermediatePercentage), 97 | alpha: CGFloat(alpha1 + (alpha2 - alpha1) * intermediatePercentage) 98 | ) 99 | 100 | return cgColor 101 | } 102 | } 103 | 104 | public func gradientColors(in size: CGFloat) -> Self { 105 | var result: Self = [] 106 | for point in 1...Int(size) { 107 | let color = self.gradientColor(at: CGFloat(point), size: size) 108 | result.append(color) 109 | } 110 | return result 111 | } 112 | } 113 | 114 | #if canImport(UIKit) 115 | extension CGColor { 116 | fileprivate static var clear: CGColor { UIColor.clear.cgColor } 117 | } 118 | #endif 119 | 120 | -------------------------------------------------------------------------------- /Sources/DominantColors/ImageColorError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageColorError.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 03.09.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ImageColorError: Error { 11 | /// The `CIImage` instance could not be created. 12 | case ciImageFailure 13 | 14 | /// The `CGImage` instance could not be created. 15 | case cgImageFailure 16 | 17 | /// Failed to get the pixel data from the `CGImage` instance. 18 | case cgImageDataFailure 19 | 20 | /// An error happened during the creation of the image after applying the filter. 21 | case outputImageFailure 22 | 23 | /// Can't create CIFilter(name: "FILTER_KEY") 24 | case ciFilterCreateFailure(filter: String) 25 | 26 | /// The singleton CFNull (special memory address) object is nil 27 | case kCFNullFailure 28 | 29 | /// The resolution of the image is less than the requested number of colors 30 | case lowResolutionFailure 31 | 32 | /// The `CGColor` instance could not be created. 33 | case cgColorFailure 34 | } 35 | 36 | extension ImageColorError: LocalizedError { 37 | var errorDescription: String? { 38 | switch self { 39 | case .ciImageFailure: 40 | return "Failed to get a `CIImage` instance." 41 | case .cgImageFailure: 42 | return "Failed to get a `CGImage` instance." 43 | case .cgImageDataFailure: 44 | return "Failed to get image data." 45 | case .outputImageFailure: 46 | return "Could not get the output image from the filter." 47 | case .ciFilterCreateFailure(let filter): 48 | return "Failed to create CIFilter \(filter)." 49 | case .kCFNullFailure: 50 | return "The singleton CFNull (special memory address) object is nil." 51 | case .lowResolutionFailure: 52 | return "The resolution of the image is less than the requested number of colors" 53 | case .cgColorFailure: 54 | return "Failed create cg color or color space for color." 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/DominantColors/ImageFIlter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageFilter.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 28.04.2024. 6 | // 7 | 8 | import CoreImage 9 | #if os(OSX) 10 | import AppKit.NSImage 11 | #elseif os(iOS) 12 | import UIKit.UIImage 13 | #endif 14 | 15 | class ImageFilter { 16 | /// Resize the image based on the requested quality 17 | static func resizeFilter(image: CGImage, by quality: DominantColorQuality = .fair) -> CGImage { 18 | let targetSize = quality.targetSize(for: image.resolution) 19 | 20 | let resizedImage = image.resize(to: targetSize) 21 | 22 | return resizedImage 23 | } 24 | 25 | /// Makes an image blocky by mapping the image to colored squares whose color is defined by the replaced pixels.. 26 | /// https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/filter/ci/CIPixellate 27 | static func pixellate(image: CGImage, by quality: DominantColorQuality = .fair) throws -> CGImage { 28 | let filterName = "CIPixellate" 29 | guard let filter = CIFilter(name: filterName) 30 | else { throw ImageColorError.ciFilterCreateFailure(filter: filterName) } 31 | 32 | let beginImage = CIImage(cgImage: image) 33 | filter.setValue(beginImage, forKey: kCIInputImageKey) 34 | filter.setValue(quality.pixellateScale, forKey: kCIInputScaleKey) 35 | 36 | guard let outputImage = filter.outputImage 37 | else { throw ImageColorError.ciFilterCreateFailure(filter: filterName) } 38 | 39 | let context = CIContext() 40 | 41 | guard let outputCGImage = context.createCGImage(outputImage, from: outputImage.extent) 42 | else { throw ImageColorError.ciFilterCreateFailure(filter: filterName) } 43 | 44 | return outputCGImage 45 | } 46 | 47 | static func cropAlpha(image: CGImage) throws -> CGImage { 48 | guard let imageTrimmed = image.trimmingTransparentPixels(maximumAlphaChannel: 150) 49 | else { throw ImageColorError.cgImageDataFailure } 50 | 51 | return imageTrimmed 52 | } 53 | } 54 | 55 | 56 | #if os(OSX) 57 | extension ImageFilter { 58 | // Representation CFData image 59 | private static func imageRepresentation(cgImage: CGImage) throws -> CGImage { 60 | let context = CIContext() 61 | guard let dataCroppedImage = context.pngRepresentation(of: CIImage(cgImage: cgImage), format: .RGBA8, colorSpace: cgImage.colorSpace!), 62 | let nsImageCroppedImage = NSImage(data: dataCroppedImage), 63 | let outputCGImage = nsImageCroppedImage.cgImage(forProposedRect: nil, context: nil, hints: nil) 64 | else { throw ImageColorError.ciFilterCreateFailure(filter: #function) } 65 | 66 | return outputCGImage 67 | } 68 | } 69 | #elseif os(iOS) 70 | extension ImageFilter { 71 | // Representation CFData image 72 | private static func imageRepresentation(cgImage: CGImage) throws -> CGImage { 73 | let context = CIContext() 74 | guard let dataCroppedImage = context.pngRepresentation(of: CIImage(cgImage: cgImage), format: .RGBA8, colorSpace: cgImage.colorSpace!), 75 | let uiImageCroppedImage = UIImage(data: dataCroppedImage), 76 | let outputCGImage = uiImageCroppedImage.cgImage 77 | else { throw ImageColorError.ciFilterCreateFailure(filter: #function) } 78 | 79 | return outputCGImage 80 | } 81 | } 82 | #endif 83 | 84 | -------------------------------------------------------------------------------- /Sources/DominantColors/ImageTrimAlpha.swift: -------------------------------------------------------------------------------- 1 | // Image+Trim.swift 2 | // 3 | // Copyright © 2020 Christopher Zielinski. 4 | // https://gist.github.com/chriszielinski/aec9a2f2ba54745dc715dd55f5718177 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | #if canImport(UIKit) 24 | import UIKit 25 | #else 26 | import AppKit 27 | #endif 28 | 29 | extension CGImage { 30 | /// Crops the insets of transparency around the image. 31 | /// 32 | /// - Parameters: 33 | /// - maximumAlphaChannel: The maximum alpha channel value to consider _transparent_ and thus crop. Any alpha value 34 | /// strictly greater than `maximumAlphaChannel` will be considered opaque. 35 | func trimmingTransparentPixels(maximumAlphaChannel: UInt8 = 0) -> CGImage? { 36 | return _CGImageTransparencyTrimmer(image: self, maximumAlphaChannel: maximumAlphaChannel)?.trim() 37 | } 38 | } 39 | private struct _CGImageTransparencyTrimmer { 40 | let image: CGImage 41 | let maximumAlphaChannel: UInt8 42 | let cgContext: CGContext 43 | let zeroByteBlock: UnsafeMutableRawPointer 44 | let pixelRowRange: Range 45 | let pixelColumnRange: Range 46 | init?(image: CGImage, maximumAlphaChannel: UInt8) { 47 | guard let cgContext = CGContext(data: nil, 48 | width: image.width, 49 | height: image.height, 50 | bitsPerComponent: 8, 51 | bytesPerRow: 0, 52 | space: CGColorSpaceCreateDeviceGray(), 53 | bitmapInfo: CGImageAlphaInfo.alphaOnly.rawValue), 54 | cgContext.data != nil 55 | else { return nil } 56 | cgContext.draw(image, 57 | in: CGRect(origin: .zero, 58 | size: CGSize(width: image.width, 59 | height: image.height))) 60 | guard let zeroByteBlock = calloc(image.width, MemoryLayout.size) 61 | else { return nil } 62 | self.image = image 63 | self.maximumAlphaChannel = maximumAlphaChannel 64 | self.cgContext = cgContext 65 | self.zeroByteBlock = zeroByteBlock 66 | pixelRowRange = 0.. CGImage? { 70 | guard let topInset = firstOpaquePixelRow(in: pixelRowRange), 71 | let bottomOpaqueRow = firstOpaquePixelRow(in: pixelRowRange.reversed()), 72 | let leftInset = firstOpaquePixelColumn(in: pixelColumnRange), 73 | let rightOpaqueColumn = firstOpaquePixelColumn(in: pixelColumnRange.reversed()) 74 | else { return nil } 75 | let bottomInset = (image.height - 1) - bottomOpaqueRow 76 | let rightInset = (image.width - 1) - rightOpaqueColumn 77 | guard !(topInset == 0 && bottomInset == 0 && leftInset == 0 && rightInset == 0) 78 | else { return image } 79 | return image.cropping(to: CGRect(origin: CGPoint(x: leftInset, y: topInset), 80 | size: CGSize(width: image.width - (leftInset + rightInset), 81 | height: image.height - (topInset + bottomInset)))) 82 | } 83 | @inlinable 84 | func isPixelOpaque(column: Int, row: Int) -> Bool { 85 | // Sanity check: It is safe to get the data pointer in iOS 4.0+ and macOS 10.6+ only. 86 | assert(cgContext.data != nil) 87 | return cgContext.data!.load(fromByteOffset: (row * cgContext.bytesPerRow) + column, as: UInt8.self) 88 | > maximumAlphaChannel 89 | } 90 | @inlinable 91 | func isPixelRowTransparent(_ row: Int) -> Bool { 92 | assert(cgContext.data != nil) 93 | // `memcmp` will efficiently check if the entire pixel row has zero alpha values 94 | return memcmp(cgContext.data! + (row * cgContext.bytesPerRow), zeroByteBlock, image.width) == 0 95 | // When the entire row is NOT zeroed, we proceed to check each pixel's alpha 96 | // value individually until we locate the first "opaque" pixel (very ~not~ efficient). 97 | || !pixelColumnRange.contains(where: { isPixelOpaque(column: $0, row: row) }) 98 | } 99 | @inlinable 100 | func firstOpaquePixelRow(in rowRange: T) -> Int? where T.Element == Int { 101 | return rowRange.first(where: { !isPixelRowTransparent($0) }) 102 | } 103 | @inlinable 104 | func firstOpaquePixelColumn(in columnRange: T) -> Int? where T.Element == Int { 105 | return columnRange.first(where: { column in 106 | pixelRowRange.contains(where: { isPixelOpaque(column: column, row: $0) }) 107 | }) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/DominantColors/RelativeLuminance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RelativeLuminance.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 04.09.2023. 6 | // 7 | 8 | import CoreImage 9 | 10 | extension CGColor { 11 | 12 | /// Computes the relative luminance of the color. 13 | /// This assume that the color is using the sRGB color space. 14 | /// This is the relative brightness, normalized where 0 is black and 1 is white. 15 | public var relativeLuminance: CGFloat { 16 | func toLinear(colorAttribute: CGFloat) -> CGFloat { 17 | if colorAttribute <= 0.03928 { 18 | return colorAttribute / 12.92 19 | } else { 20 | return pow((colorAttribute + 0.055) / 1.055, 2.4) 21 | } 22 | } 23 | 24 | let linearR = toLinear(colorAttribute: red) 25 | let linearG = toLinear(colorAttribute: green) 26 | let linearB = toLinear(colorAttribute: blue) 27 | 28 | let relativeLuminance = 0.2126 * linearR + 0.7152 * linearG + 0.0722 * linearB 29 | 30 | return relativeLuminance.rounded(.toNearestOrAwayFromZero, precision: 1000) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/AverageColorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AverageColorTests.swift 3 | // ColorKitTests 4 | // 5 | // Created by Boris Emorine on 5/15/20. 6 | // Copyright © 2020 BorisEmorine. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import DominantColors 11 | 12 | #if os(OSX) 13 | class AverageColorTests: XCTestCase { 14 | 15 | static let tolerance: CGFloat = 0.5 16 | 17 | /// It should compute a green average color for a green image. 18 | func testGreenImage() throws { 19 | let name = NSImage.Name("Green_Square") 20 | let nsImage = Bundle.module.image(forResource: name) 21 | let cgImage = nsImage!.cgImage(forProposedRect: nil, context: nil, hints: nil)! 22 | 23 | let averageColor = try AverageColor.averageColor(image: cgImage) 24 | 25 | let distance = averageColor.difference(from: CGColor(red: .zero, green: 1, blue: .zero, alpha: 1.0)) 26 | XCTAssertLessThan(distance.associatedValue, AverageColorTests.tolerance) 27 | } 28 | 29 | /// It should compute a purple average color for a purple image. 30 | func testPurpleImage() throws { 31 | let name = NSImage.Name("Purple_Square") 32 | let nsImage = Bundle.module.image(forResource: name) 33 | let cgImage = nsImage!.cgImage(forProposedRect: nil, context: nil, hints: nil)! 34 | 35 | let averageColor = try AverageColor.averageColor(image: cgImage) 36 | 37 | let expectedPurple = CGColor(red: 208.0 / 255.0, green: 0.0 / 255.0, blue: 255.0 / 255.0, alpha: 1.0) 38 | let distance = averageColor.difference(from: expectedPurple) 39 | XCTAssertLessThan(distance.associatedValue, AverageColorTests.tolerance) 40 | } 41 | 42 | /// It should compute a gray average color for a black & white image. 43 | func testBlackWhiteImage() throws { 44 | let name = NSImage.Name("Black_White_Square") 45 | let nsImage = Bundle.module.image(forResource: name) 46 | let cgImage = nsImage!.cgImage(forProposedRect: nil, context: nil, hints: nil)! 47 | 48 | let averageColor = try AverageColor.averageColor(image: cgImage) 49 | 50 | let expectedGray = CGColor(red: 188.0 / 255.0, green: 188.0 / 255.0, blue: 188.0 / 255.0, alpha: 1.0) 51 | let distance = averageColor.difference(from: expectedGray) 52 | XCTAssertLessThan(distance.associatedValue, AverageColorTests.tolerance) 53 | } 54 | 55 | } 56 | #endif 57 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/CGFloatExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGFloatExtensionsTests.swift 3 | // ColorKitTests 4 | // 5 | // Created by Boris Emorine on 2/26/20. 6 | // Copyright © 2020 BorisEmorine. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import DominantColors 11 | 12 | class CGFloatExtensionsTests: XCTestCase { 13 | 14 | func testRoundedWithPrecision10() { 15 | let sut: CGFloat = 100.39999999 16 | let roundedSut = sut.rounded(precision: 10) 17 | XCTAssertEqual(roundedSut, 100.4) 18 | } 19 | 20 | func testRoundedWithPrecision() { 21 | let sut: CGFloat = 1.49999999 22 | let roundedSut = sut.rounded(precision: 10) 23 | XCTAssertEqual(roundedSut, 1.5) 24 | } 25 | 26 | func testRoundedWithPrecision100() { 27 | let sut: CGFloat = 100.39999999 28 | let roundedSut = sut.rounded(precision: 100) 29 | XCTAssertEqual(roundedSut, 100.40) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/CGSizeExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGSizeExtensionsTests.swift 3 | // ColorKitTests 4 | // 5 | // Created by Boris Emorine on 5/30/20. 6 | // Copyright © 2020 BorisEmorine. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import DominantColors 11 | 12 | class CGSizeExtensionsTests: XCTestCase { 13 | 14 | /// A simple test with hard coded values where the target size is smaller than the original size. 15 | func testSimpleSmaller() { 16 | let originalSize = CGSize(width: 100, height: 100) 17 | let targetSize = originalSize.transformToFit(in: 100) 18 | 19 | let expectedSize = CGSize(width: 10, height: 10) 20 | XCTAssertEqual(targetSize, expectedSize) 21 | } 22 | 23 | /// A simple test with hard coded values where the target size is greater than the original size. 24 | func testSimpleGreater() { 25 | let originalSize = CGSize(width: 10, height: 10) 26 | let targetSize = originalSize.transformToFit(in: 10_000) 27 | 28 | let expectedSize = CGSize(width: 100, height: 100) 29 | XCTAssertEqual(targetSize, expectedSize) 30 | } 31 | 32 | /// It should return a target size with the expected area and keeping the same size ratio, when the target area is smaller than the original area. 33 | func testSmaller() { 34 | let originalSize = CGSize(width: 1024, height: 800) 35 | let targetArea: CGFloat = originalSize.area / CGFloat.random(in: 1...4) 36 | let targetSize = originalSize.transformToFit(in: targetArea) 37 | 38 | XCTAssertEqual(originalSize.width / originalSize.height, targetSize.width / targetSize.height, accuracy: 0.01) 39 | XCTAssertEqual(targetArea, targetSize.width * targetSize.height, accuracy: 0.01) 40 | } 41 | 42 | /// It should return a target size with the expected area and keeping the same size ratio, when the target area is greater than the original area. 43 | func testGreater() { 44 | let originalSize = CGSize(width: 1024, height: 800) 45 | let targetArea: CGFloat = originalSize.area * CGFloat.random(in: 1...4) 46 | let targetSize = originalSize.transformToFit(in: targetArea) 47 | 48 | XCTAssertEqual(originalSize.width / originalSize.height, targetSize.width / targetSize.height, accuracy: 0.01) 49 | XCTAssertEqual(targetArea, targetSize.width * targetSize.height, accuracy: 0.01) 50 | } 51 | 52 | /// It should return a target size with the expected area and keeping the same size ratio. 53 | func testRandom() { 54 | let originalSize = CGSize(width: CGFloat.random(in: 0...100000), height: CGFloat.random(in: 0...100000)) 55 | let targetArea: CGFloat = originalSize.area * CGFloat.random(in: 0...2) 56 | let targetSize = originalSize.transformToFit(in: targetArea) 57 | 58 | XCTAssertEqual(originalSize.width / originalSize.height, targetSize.width / targetSize.height, accuracy: 0.01) 59 | XCTAssertEqual(targetArea, targetSize.width * targetSize.height, accuracy: 0.01) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/ColorDifferenceTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorDifferenceTest.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 10.05.2024. 6 | // 7 | // Used Color Difference Calculator 8 | // http://www.brucelindbloom.com/index.html?ColorCalculator.html 9 | // 10 | // Lab Reference red: Lab(53.2408, 80.0923, 67.2031) 11 | // Lab Sample green: (87.7347, -86.1827, 83.1793) 12 | // CIE 1976: 170.56507 13 | // CIE 1994: 73.430412 (Graphic Arts) 14 | // CIE 2000: 86.608180 (1:1:1) 15 | // CMC: 108.44909 (1:1) 16 | 17 | import XCTest 18 | @testable import DominantColors 19 | 20 | final class ColorDifferenceTest: XCTestCase { 21 | 22 | func testDeltaCIE76() throws { 23 | let red = CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1) 24 | let green = CGColor(srgbRed: 0, green: 1, blue: 0, alpha: 1) 25 | 26 | let deltaCIE76 = red.difference(from: green, using: .CIE76) 27 | XCTAssertEqual(deltaCIE76.associatedValue, 170, accuracy: 1) 28 | } 29 | 30 | func testDeltaCIE94() throws { 31 | let red = CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1) 32 | let green = CGColor(srgbRed: 0, green: 1, blue: 0, alpha: 1) 33 | 34 | let deltaCIE94 = red.difference(from: green, using: .CIE94) 35 | XCTAssertEqual(deltaCIE94.associatedValue, 73, accuracy: 1) 36 | } 37 | 38 | func testDeltaCIEDE2000() throws { 39 | let red = CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1) 40 | let green = CGColor(srgbRed: 0, green: 1, blue: 0, alpha: 1) 41 | 42 | let deltaCIEDE2000 = red.difference(from: green, using: .CIEDE2000) 43 | XCTAssertEqual(deltaCIEDE2000.associatedValue, 86, accuracy: 1) 44 | } 45 | 46 | func testDeltaCMC() throws { 47 | let red = CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1) 48 | let green = CGColor(srgbRed: 0, green: 1, blue: 0, alpha: 1) 49 | 50 | let deltaCMC = red.difference(from: green, using: .CMC) 51 | XCTAssertEqual(deltaCMC.associatedValue, 108, accuracy: 1) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/ColorFrequencyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorFrequencyTests.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 01.05.2024. 6 | // 7 | 8 | import XCTest 9 | @testable import DominantColors 10 | 11 | final class ColorFrequencyTests: XCTestCase { 12 | 13 | func testFrequency() throws { 14 | let red = CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1.0) 15 | let colorFrequencyRed = ColorFrequency(color: red, count: 1) 16 | XCTAssertEqual(colorFrequencyRed.frequency, 1) 17 | 18 | let green = CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1.0) 19 | let colorFrequencyGreen = ColorFrequency(color: green, count: 2) 20 | XCTAssertEqual(colorFrequencyGreen.frequency, 2) 21 | 22 | let blue = CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 1.0) 23 | let colorFrequencyBlue = ColorFrequency(color: blue, count: 3) 24 | XCTAssertEqual(colorFrequencyBlue.frequency, 3) 25 | } 26 | 27 | func testNormalFactor() throws { 28 | // HSL(0, 75, 50) 29 | let color = CGColor(srgbRed: 223 / 255, green: 32 / 255, blue: 32 / 255, alpha: 1.0) 30 | let colorFrequency = ColorFrequency(color: color, count: 1) 31 | let normal = colorFrequency.normal 32 | 33 | XCTAssertEqual(colorFrequency.frequency, 1) 34 | XCTAssertEqual(colorFrequency.normalSaturationFactor, 1) 35 | XCTAssertEqual(colorFrequency.normalLightnessFactor, 1) 36 | XCTAssertEqual(normal, colorFrequency.frequency * 1 * 1) 37 | } 38 | 39 | func testNoNormalFactor() throws { 40 | // HSL(0, 0, 0) 41 | let color = CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 1.0) 42 | let colorFrequency = ColorFrequency(color: color, count: 1) 43 | let normal = colorFrequency.normal 44 | 45 | XCTAssertEqual(colorFrequency.frequency, 1) 46 | XCTAssertEqual(colorFrequency.normalSaturationFactor, 0) 47 | XCTAssertEqual(colorFrequency.normalLightnessFactor, 0) 48 | XCTAssertEqual(normal, colorFrequency.frequency * 0 * 0) 49 | } 50 | 51 | func testAlmostNormalFactor() throws { 52 | // HSL(0, 25, 75) 53 | let color = CGColor(srgbRed: 207 / 255, green: 175 / 255, blue: 175 / 255, alpha: 1.0) 54 | let colorFrequency = ColorFrequency(color: color, count: 1) 55 | let normal = colorFrequency.normal 56 | 57 | XCTAssertEqual(colorFrequency.frequency, 1) 58 | XCTAssertEqual(colorFrequency.normalSaturationFactor, 0.33) 59 | XCTAssertEqual(colorFrequency.normalLightnessFactor, 0.5) 60 | XCTAssertEqual(normal, colorFrequency.frequency * 0.33 * 0.5) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/ComplementaryColorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComplementaryColorTests.swift 3 | // ColorKitTests 4 | // 5 | // Created by Boris Emorine on 3/18/20. 6 | // Copyright © 2020 BorisEmorine. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import DominantColors 11 | 12 | class ComplementaryColorTests: XCTestCase { 13 | 14 | func testBlack() { 15 | let black = CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 1) 16 | let complementaryColor = black.complementaryColor 17 | XCTAssertEqual(complementaryColor.components, CGColor(red: 1, green: 1, blue: 1, alpha: 1.0).components) 18 | } 19 | 20 | func testWhite() { 21 | let white = CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1) 22 | let complementaryColor = white.complementaryColor 23 | XCTAssertEqual(complementaryColor.components, CGColor(red: 0, green: 0, blue: 0, alpha: 1.0).components) 24 | } 25 | 26 | func testBlue() { 27 | let blue = CGColor(red: .zero, green: .zero, blue: 1, alpha: 1.0) 28 | let complementaryColor = blue.complementaryColor 29 | XCTAssertEqual(complementaryColor.components, CGColor(red: 255.0 / 255.0, green: 255.0 / 255.0, blue: 0, alpha: 1.0).components) 30 | } 31 | 32 | func testYellow() { 33 | let yellow = CGColor(red: 255.0 / 255.0, green: 255.0 / 255.0, blue: 0, alpha: 1.0) 34 | let complementaryColor = yellow.complementaryColor 35 | XCTAssertEqual(complementaryColor.components, CGColor(red: .zero, green: .zero, blue: 255, alpha: 1.0).components) 36 | } 37 | 38 | func testRed() { 39 | let red = CGColor(red: 1, green: .zero, blue: .zero, alpha: 1.0) 40 | let complementaryColor = red.complementaryColor 41 | XCTAssertEqual(complementaryColor.components, CGColor(red: 0.0 / 255.0, green: 255.0 / 255.0, blue: 255.0 / 255.0, alpha: 1.0).components) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/ContrastColorsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContrastColorsTests.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 12.05.2024. 6 | // 7 | 8 | import XCTest 9 | @testable import DominantColors 10 | 11 | final class ContrastColorsTests: XCTestCase { 12 | 13 | // MARK: - Colors 14 | 15 | func testPaletteNoColors() { 16 | XCTAssertNil(ContrastColors(colors: [])) 17 | } 18 | 19 | func testPaletteOneColor() { 20 | XCTAssertNil(ContrastColors(colors: [.green])) 21 | } 22 | 23 | func testPaletteSameColors() { 24 | XCTAssertNil(ContrastColors(colors: [.green, .green, .green, .green])) 25 | } 26 | 27 | func testPaletteBlackWhiteColors() { 28 | let colorPalette = ContrastColors(colors: [.black, .white]) 29 | XCTAssertEqual(colorPalette?.background, .black) 30 | XCTAssertEqual(colorPalette?.primary, .white) 31 | XCTAssertNil(colorPalette?.secondary) 32 | } 33 | 34 | func testPaletteBlackWhiteColorsBright() { 35 | let colorPalette = ContrastColors(colors: [.black, .white], darkBackground: false) 36 | XCTAssertEqual(colorPalette?.background, .white) 37 | XCTAssertEqual(colorPalette?.primary, .black) 38 | XCTAssertNil(colorPalette?.secondary) 39 | } 40 | 41 | func testCloseColors() { 42 | XCTAssertNil(ContrastColors(colors: [.blue, CGColor(red: 0, green: 0, blue: 0.8, alpha: 1.0)])) 43 | } 44 | 45 | func testRealUseCase() { 46 | let darkBlue = CGColor(red: 0.0 / 255.0, green: 120.0 / 255.0, blue: 190.0 / 255.0, alpha: 1.0) 47 | let brightBlue = CGColor(red: 110.0 / 255.0, green: 178.0 / 255.0, blue: 200.0 / 255.0, alpha: 1.0) 48 | let orange = CGColor(red: 203.0 / 255.0, green: 179.0 / 255.0, blue: 121.0 / 255.0, alpha: 1.0) 49 | let colorPalette = ContrastColors(colors: [darkBlue, brightBlue, orange], ignoreContrastRatio: true) 50 | XCTAssertEqual(colorPalette?.background, darkBlue) 51 | XCTAssertEqual(colorPalette?.primary, orange) 52 | XCTAssertEqual(colorPalette?.secondary, brightBlue) 53 | } 54 | 55 | func testRealUseCase2() { 56 | let red = CGColor(red: 255.0 / 255.0, green: 21.0 / 255.0, blue: 13.0 / 255.0, alpha: 1.0) 57 | let darkBlue = CGColor(red: 76.0 / 255.0, green: 101.0 / 255.0, blue: 122.0 / 255.0, alpha: 1.0) 58 | let white = CGColor.white 59 | let colorPalette = ContrastColors(colors: [red, darkBlue, white], darkBackground: false) 60 | XCTAssertEqual(colorPalette?.background, white) 61 | XCTAssertEqual(colorPalette?.primary, darkBlue) 62 | XCTAssertEqual(colorPalette?.secondary, red) 63 | } 64 | 65 | // MARK: - Ordered Colors 66 | 67 | func testPaletteNoOrderedColors() { 68 | XCTAssertNil(ContrastColors(orderedColors: [])) 69 | } 70 | 71 | func testPaletteOneOrderedColor() { 72 | XCTAssertNil(ContrastColors(orderedColors: [.green])) 73 | } 74 | 75 | func testPaletteSameOrderedColors() { 76 | XCTAssertNil(ContrastColors(orderedColors: [.green, .green, .green, .green])) 77 | } 78 | 79 | func testPaletteBlackWhiteOrderedColors() { 80 | let colorPalette = ContrastColors(orderedColors: [.black, .white]) 81 | XCTAssertEqual(colorPalette?.background, .black) 82 | XCTAssertEqual(colorPalette?.primary, .white) 83 | XCTAssertNil(colorPalette?.secondary) 84 | } 85 | 86 | func testPaletteWhiteBlackOrderedColorsBright() { 87 | let colorPalette = ContrastColors(orderedColors: [.white, .black], darkBackground: false) 88 | XCTAssertEqual(colorPalette?.background, .white) 89 | XCTAssertEqual(colorPalette?.primary, .black) 90 | XCTAssertNil(colorPalette?.secondary) 91 | } 92 | 93 | func testPaletteBlackWhiteOrderedColorsBright() { 94 | let colorPalette = ContrastColors(orderedColors: [.black, .white], darkBackground: false) 95 | XCTAssertEqual(colorPalette?.background, .black) 96 | XCTAssertEqual(colorPalette?.primary, .white) 97 | XCTAssertNil(colorPalette?.secondary) 98 | } 99 | 100 | func testCloseOrderedColors() { 101 | XCTAssertNil(ContrastColors(orderedColors: [.blue, CGColor(red: 0, green: 0, blue: 0.8, alpha: 1.0)])) 102 | } 103 | 104 | func testRealUseCaseOrdered() { 105 | let darkBlue = CGColor(red: 0.0 / 255.0, green: 120.0 / 255.0, blue: 190.0 / 255.0, alpha: 1.0) 106 | let brightBlue = CGColor(red: 110.0 / 255.0, green: 178.0 / 255.0, blue: 200.0 / 255.0, alpha: 1.0) 107 | let orange = CGColor(red: 203.0 / 255.0, green: 179.0 / 255.0, blue: 121.0 / 255.0, alpha: 1.0) 108 | let colorPalette = ContrastColors(orderedColors: [darkBlue, brightBlue, orange], ignoreContrastRatio: true) 109 | XCTAssertEqual(colorPalette?.background, darkBlue) 110 | XCTAssertEqual(colorPalette?.primary, brightBlue) 111 | XCTAssertEqual(colorPalette?.secondary, orange) 112 | } 113 | 114 | func testRealUseCase2Ordered() { 115 | let red = CGColor(red: 255.0 / 255.0, green: 21.0 / 255.0, blue: 13.0 / 255.0, alpha: 1.0) 116 | let darkBlue = CGColor(red: 76.0 / 255.0, green: 101.0 / 255.0, blue: 122.0 / 255.0, alpha: 1.0) 117 | let white = CGColor.white 118 | let colorPalette = ContrastColors(orderedColors: [red, darkBlue, white], darkBackground: false) 119 | XCTAssertEqual(colorPalette?.background, red) 120 | XCTAssertEqual(colorPalette?.primary, white) 121 | XCTAssertNil(colorPalette?.secondary) 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/ContrastRatioTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContrastRatioTests.swift 3 | // ColorKitTests 4 | // 5 | // Created by Boris Emorine on 3/13/20. 6 | // Copyright © 2020 BorisEmorine. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import DominantColors 11 | 12 | class ContrastRatioTests: XCTestCase { 13 | 14 | func testBlackWhite() { 15 | let color = CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1) 16 | let backgroundColor = CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 1) 17 | let contrastRatioResult = color.contrastRatio(with: backgroundColor) 18 | XCTAssertEqual(contrastRatioResult.associatedValue, 21.0) 19 | } 20 | 21 | func testWhiteBlack() { 22 | let color = CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 1) 23 | let backgroundColor = CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1) 24 | let contrastRatioResult = color.contrastRatio(with: backgroundColor) 25 | XCTAssertEqual(contrastRatioResult.associatedValue, 21.0) 26 | } 27 | 28 | func testOrangeOrangeClose() { 29 | let color = CGColor(red: 243.0 / 255.0, green: 120.0 / 255.0, blue: 9.0 / 255.0, alpha: 1.0) 30 | let backgroundColor = CGColor(red: 222.0 / 255.0, green: 100.0 / 255.0, blue: 10.0 / 255.0, alpha: 1.0) 31 | let contrastRatioResult = color.contrastRatio(with: backgroundColor) 32 | XCTAssertEqual(contrastRatioResult.associatedValue, 1.26) 33 | } 34 | 35 | func testOrangeOrange() { 36 | let color = CGColor(red: 243.0 / 255.0, green: 120.0 / 255.0, blue: 9.0 / 255.0, alpha: 1.0) 37 | let backgroundColor = CGColor(red: 243.0 / 255.0, green: 120.0 / 255.0, blue: 9.0 / 255.0, alpha: 1.0) 38 | let contrastRatioResult = color.contrastRatio(with: backgroundColor) 39 | XCTAssertEqual(contrastRatioResult.associatedValue, 1.0) 40 | } 41 | 42 | func testGreenPurple() { 43 | let green = CGColor(red: 0.0 / 255.0, green: 255.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0) 44 | let blue = CGColor(red: 0.0 / 255.0, green: 0.0 / 255.0, blue: 255.0 / 255.0, alpha: 1.0) 45 | let contrastRatioResult = green.contrastRatio(with: blue) 46 | XCTAssertEqual(contrastRatioResult.associatedValue, 6.27) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/DominantColorQualityTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DominantColorQualityTests.swift 3 | // ColorKitTests 4 | // 5 | // Created by Boris Emorine on 5/30/20. 6 | // Copyright © 2020 BorisEmorine. All rights reserved. 7 | // 8 | #if os(OSX) 9 | import XCTest 10 | @testable import DominantColors 11 | 12 | class DominantColorQualityTests: XCTestCase { 13 | 14 | // func testImageFilter() throws { 15 | // let name = NSImage.Name("LittleMissSunshine") 16 | // let nsImage = Bundle.module.image(forResource: name) 17 | // let cgImage = nsImage!.cgImage(forProposedRect: nil, context: nil, hints: nil)! 18 | // 19 | // let colors = try DominantColors.dominantColors(image: cgImage, quality: .high) 20 | // } 21 | 22 | /// It should return the exact same size (original size) if the quality is set to best. 23 | func testBestQuality() { 24 | let quality = DominantColorQuality.best 25 | let originalSize = CGSize(width: CGFloat.random(in: 0...10000), height: CGFloat.random(in: 0...10000)) 26 | let targetSize = quality.targetSize(for: originalSize) 27 | 28 | XCTAssertEqual(targetSize, originalSize) 29 | } 30 | 31 | /// It should return the exact same size (original size) if the original size is smaller than the size we're trying to reach. 32 | func testLowerArea() { 33 | let quality = DominantColorQuality.fair 34 | let originalSize = CGSize(width: 1, height: 1) 35 | let targetSize = quality.targetSize(for: originalSize) 36 | 37 | XCTAssertEqual(targetSize, originalSize) 38 | } 39 | 40 | } 41 | #endif 42 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/DominantColorsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DominantColorsTests.swift 3 | // ColorKitTests 4 | // 5 | // Created by Boris Emorine on 5/19/20. 6 | // Copyright © 2020 BorisEmorine. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import DominantColors 11 | 12 | #if os(OSX) 13 | class DominantColorsTests: XCTestCase { 14 | 15 | func testGreenImage() throws { 16 | let name = NSImage.Name("Green_Square") 17 | let nsImage = Bundle.module.image(forResource: name) 18 | let cgImage = nsImage!.cgImage(forProposedRect: nil, context: nil, hints: nil)! 19 | let dominantColors = try DominantColors.dominantColorFrequencies(image: cgImage, quality: .best) 20 | 21 | XCTAssertEqual(dominantColors.count, 1) 22 | 23 | let green = CGColor(colorSpace: CGColorSpace(name: CGColorSpace.sRGB)!, components: [0, 1, 0, 1])! 24 | XCTAssertEqual(dominantColors.first!.color, green) 25 | guard let distance = dominantColors.first?.color.difference(from: green) 26 | else { 27 | XCTFail("Could not get distance from dominant color.") 28 | return 29 | } 30 | XCTAssertLessThan(distance.associatedValue, AverageColorTests.tolerance) 31 | XCTAssertEqual(dominantColors.first?.frequency, 1.0) 32 | } 33 | 34 | func testBlackWhiteImage() throws { 35 | let name = NSImage.Name("Black_White_Square") 36 | let nsImage = Bundle.module.image(forResource: name) 37 | let cgImage = nsImage!.cgImage(forProposedRect: nil, context: nil, hints: nil)! 38 | let colorFrequencies = try DominantColors.dominantColorFrequencies(image: cgImage, quality: .best) 39 | let dominantColors = colorFrequencies.map({ $0.color }) 40 | 41 | XCTAssertEqual(dominantColors.count, 2) 42 | 43 | let black = CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 1) 44 | XCTAssertTrue(dominantColors.contains(black)) 45 | 46 | let white = CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1) 47 | XCTAssertTrue(dominantColors.contains(white)) 48 | 49 | verifySorted(colorsFrequencies: colorFrequencies) 50 | 51 | XCTAssertEqual(colorFrequencies.first?.frequency, 0.5) 52 | XCTAssertEqual(colorFrequencies.last?.frequency, 0.5) 53 | } 54 | 55 | func testRemoveBlackWhite() throws { 56 | let name = NSImage.Name("Black_White_Red_Green_Blue_Grey") 57 | let nsImage = Bundle.module.image(forResource: name) 58 | let cgImage = nsImage!.cgImage(forProposedRect: nil, context: nil, hints: nil)! 59 | let colorFrequencies = try DominantColors.dominantColorFrequencies(image: cgImage, quality: .best, options: [.excludeBlack, .excludeWhite]) 60 | 61 | let extractColorAfterBlackWhiteFilter = 5 62 | 63 | XCTAssertEqual(colorFrequencies.count, extractColorAfterBlackWhiteFilter) 64 | } 65 | 66 | func testRedBlueGreenImage() throws { 67 | let name = NSImage.Name("Red_Green_Blue") 68 | let nsImage = Bundle.module.image(forResource: name) 69 | let cgImage = nsImage!.cgImage(forProposedRect: nil, context: nil, hints: nil)! 70 | let colorFrequencies = try DominantColors.dominantColorFrequencies(image: cgImage, quality: .best) 71 | let dominantColors = colorFrequencies.map({ $0.color }) 72 | 73 | XCTAssertEqual(dominantColors.count, 3) 74 | XCTAssertTrue(dominantColors.contains(CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1))) 75 | XCTAssertTrue(dominantColors.contains(CGColor(srgbRed: 0, green: 1, blue: 0, alpha: 1))) 76 | XCTAssertTrue(dominantColors.contains(CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 1))) 77 | verifySorted(colorsFrequencies: colorFrequencies) 78 | } 79 | 80 | func testRedBlueGreenBlack() throws { 81 | let name = NSImage.Name("Red_Green_Blue_Black_Mini") 82 | let nsImage = Bundle.module.image(forResource: name) 83 | let cgImage = nsImage!.cgImage(forProposedRect: nil, context: nil, hints: nil)! 84 | let colorFrequencies = try DominantColors.dominantColorFrequencies(image: cgImage, quality: .best) 85 | let dominantColors = colorFrequencies.map({ $0.color }) 86 | 87 | XCTAssertEqual(dominantColors.count, 4) 88 | XCTAssertTrue(dominantColors.contains(CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 1))) 89 | XCTAssertTrue(dominantColors.contains(CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1))) 90 | XCTAssertTrue(dominantColors.contains(CGColor(srgbRed: 0, green: 1, blue: 0, alpha: 1))) 91 | XCTAssertTrue(dominantColors.contains(CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 1))) 92 | verifySorted(colorsFrequencies: colorFrequencies) 93 | } 94 | 95 | func testRedBlueGreenRandom() throws { 96 | let name = NSImage.Name("Red_Green_Blue_Random_Mini") 97 | let nsImage = Bundle.module.image(forResource: name) 98 | let cgImage = nsImage!.cgImage(forProposedRect: nil, context: nil, hints: nil)! 99 | let colorFrequencies = try DominantColors.dominantColorFrequencies(image: cgImage, quality: .best) 100 | let dominantColors = colorFrequencies.map({ $0.color }) 101 | 102 | XCTAssertTrue(dominantColors.contains(CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1))) 103 | XCTAssertTrue(dominantColors.contains(CGColor(srgbRed: 0, green: 1, blue: 0, alpha: 1))) 104 | XCTAssertTrue(dominantColors.contains(CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 1))) 105 | verifySorted(colorsFrequencies: colorFrequencies) 106 | } 107 | 108 | func testImage() throws { 109 | let name = NSImage.Name("WaterLife1") 110 | let nsImage = Bundle.module.image(forResource: name) 111 | let cgImage = nsImage!.cgImage(forProposedRect: nil, context: nil, hints: nil)! 112 | let dominantColors = try DominantColors.dominantColorFrequencies(image: cgImage, quality: .best, options: [.excludeBlack, .excludeGray, .excludeWhite]) 113 | 114 | XCTAssertGreaterThan(dominantColors.count, 12) 115 | } 116 | 117 | func verifySorted(colorsFrequencies: [ColorFrequency]) { 118 | var previousCount: CGFloat? 119 | 120 | colorsFrequencies.forEach { (colorFrequency) in 121 | guard let oldCount = previousCount else { 122 | previousCount = colorFrequency.frequency 123 | return 124 | } 125 | 126 | if oldCount < colorFrequency.frequency { 127 | XCTFail("The order of the color frenquecy is not correct.") 128 | } 129 | 130 | previousCount = colorFrequency.frequency 131 | } 132 | } 133 | } 134 | #endif 135 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/ExtractColorsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtractColorsTests.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 26.04.2024. 6 | // 7 | 8 | import XCTest 9 | @testable import DominantColors 10 | 11 | #if os(OSX) 12 | final class ExtractColorsTests: XCTestCase { 13 | 14 | func testExtractColors() throws { 15 | let name = NSImage.Name("Pixeleate_Image") 16 | let nsImage = Bundle.module.image(forResource: name) 17 | let cgImage = nsImage!.cgImage(forProposedRect: nil, context: nil, hints: nil)! 18 | 19 | let colors = try DominantColors.extractColors(cgImage) 20 | 21 | XCTAssertEqual(colors.count, 4) 22 | XCTAssertEqual(colors.count(for: RGB255(red: 235, green: 87, blue: 87)), 32 * 32) 23 | XCTAssertEqual(colors.count(for: RGB255(red: 47, green: 128, blue: 237)), 32 * 32) 24 | XCTAssertEqual(colors.count(for: RGB255(red: 33, green: 150, blue: 83)), 32 * 32) 25 | XCTAssertEqual(colors.count(for: RGB255(red: 155, green: 81, blue: 224)), 32 * 32) 26 | } 27 | 28 | func testExtractColorsFromPixellateImage() throws { 29 | let name = NSImage.Name("Pixeleate_Image") // pixel size 32 30 | let nsImage = Bundle.module.image(forResource: name) 31 | let cgImage = nsImage!.cgImage(forProposedRect: nil, context: nil, hints: nil)! 32 | 33 | let colorsCountedSet = try DominantColors.extractColors(pixellate: cgImage, pixelSize: 32) 34 | 35 | XCTAssertEqual(colorsCountedSet.count, 4) 36 | 37 | XCTAssertEqual(colorsCountedSet.count(for: RGB255(red: 235, green: 87, blue: 87)), 1) 38 | XCTAssertEqual(colorsCountedSet.count(for: RGB255(red: 47, green: 128, blue: 237)), 1) 39 | XCTAssertEqual(colorsCountedSet.count(for: RGB255(red: 33, green: 150, blue: 83)), 1) 40 | XCTAssertEqual(colorsCountedSet.count(for: RGB255(red: 155, green: 81, blue: 224)), 1) 41 | } 42 | 43 | func testGrayColorSpace() throws { 44 | let name = NSImage.Name("GrayColorSpaceImage") 45 | let nsImage = Bundle.module.image(forResource: name) 46 | let cgImage = nsImage!.cgImage(forProposedRect: nil, context: nil, hints: nil)! 47 | 48 | let colorsCountedSet = try DominantColors.extractColors(cgImage) 49 | XCTAssertEqual(colorsCountedSet.count(for: RGB255(red: 255, green: 255, blue: 255)), 1) 50 | XCTAssertEqual(colorsCountedSet.count(for: RGB255(red: 129, green: 129, blue: 129)), 2) 51 | XCTAssertEqual(colorsCountedSet.count(for: RGB255(red: 0, green: 0, blue: 0)), 1) 52 | } 53 | 54 | func testGrayColorSpacePixelate() throws { 55 | let name = NSImage.Name("ShadesGrayColorSpace") 56 | let nsImage = Bundle.module.image(forResource: name) 57 | let cgImage = nsImage!.cgImage(forProposedRect: nil, context: nil, hints: nil)! 58 | 59 | let colorsCountedSet = try DominantColors.extractColors(pixellate: cgImage, pixelSize: 50) 60 | XCTAssertEqual(colorsCountedSet.count(for: RGB255(red: 255, green: 255, blue: 255)), 1) 61 | XCTAssertEqual(colorsCountedSet.count(for: RGB255(red: 129, green: 129, blue: 129)), 1) 62 | XCTAssertEqual(colorsCountedSet.count(for: RGB255(red: 0, green: 0, blue: 0)), 1) 63 | XCTAssertEqual(colorsCountedSet.count(for: RGB255(red: 46, green: 46, blue: 46)), 1) 64 | } 65 | } 66 | #endif 67 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/FIlterBlackWhiteTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilterBlackWhiteTests.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 26.04.2024. 6 | // 7 | 8 | import XCTest 9 | import SwiftUI 10 | @testable import DominantColors 11 | 12 | final class FilterBlackWhiteTests: XCTestCase { 13 | 14 | static let blackMaxLightness: CGFloat = 10 15 | static let whiteMinLightness: CGFloat = 90 16 | 17 | static let blackMaxDeltaCIE76: CGFloat = 10 18 | 19 | func testFilterBlack() throws { 20 | var colors = [ColorFrequency]() 21 | // Create colors with brightness from 0 to 10 22 | for lightness in stride(from: 0, to: 1.1, by: 0.1) { 23 | let cgColor = CGColor(srgbRed: lightness, green: lightness, blue: lightness, alpha: 1.0) 24 | colors.append(ColorFrequency(color: cgColor, count: 1)) 25 | } 26 | 27 | XCTAssertEqual(colors.count, 11) 28 | 29 | let blackColors = DominantColors.removeBlack(max: Self.blackMaxLightness, colors: &colors).map { $0.color } 30 | XCTAssertEqual(colors.count, 9) 31 | 32 | let black = CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 1) 33 | XCTAssertTrue(blackColors.contains(black)) 34 | 35 | let almostBlack = CGColor(srgbRed: 0.1, green: 0.1, blue: 0.1, alpha: 1) 36 | XCTAssertTrue(blackColors.contains(almostBlack)) 37 | } 38 | 39 | func testFilterWhite() throws { 40 | var colors = [ColorFrequency]() 41 | // Create colors with brightness from 0 to 10 42 | for lightness in stride(from: 0, to: 1.1, by: 0.1) { 43 | let cgColor = CGColor(srgbRed: lightness, green: lightness, blue: lightness, alpha: 1.0) 44 | colors.append(ColorFrequency(color: cgColor, count: 1)) 45 | } 46 | XCTAssertEqual(colors.count, 11) 47 | 48 | let whiteColors = DominantColors.removeWhite(min: Self.whiteMinLightness, colors: &colors).map { $0.color } 49 | 50 | XCTAssertEqual(colors.count, 9) 51 | 52 | let white = CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1) 53 | XCTAssertTrue(whiteColors.contains(white)) 54 | 55 | let almostWhite = CGColor(srgbRed: 0.9, green: 0.9, blue: 0.9, alpha: 1) 56 | XCTAssertTrue(whiteColors.contains(almostWhite)) 57 | } 58 | 59 | func testFilterBlackDeltaDifference() throws { 60 | var colors = [ColorFrequency]() 61 | // Create colors with brightness from 0 to 10 62 | for lightness in stride(from: 0, to: 1.1, by: 0.1) { 63 | let cgColor = CGColor(srgbRed: lightness, green: lightness, blue: lightness, alpha: 1.0) 64 | colors.append(ColorFrequency(color: cgColor, count: 1)) 65 | } 66 | colors.sort(by: { $0.frequency > $1.frequency }) 67 | XCTAssertEqual(colors.count, 11) 68 | 69 | let blackColors = DominantColors.removeBlack(delta: Self.blackMaxDeltaCIE76, colors: &colors, using: .CIE76).map { $0.color } 70 | XCTAssertEqual(colors.count, 9) 71 | 72 | let black = CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 1.0) 73 | XCTAssertTrue(blackColors.contains(black)) 74 | 75 | let almostBlack = CGColor(srgbRed: 0.1, green: 0.1, blue: 0.1, alpha: 1.0) 76 | XCTAssertTrue(blackColors.contains(almostBlack)) 77 | } 78 | 79 | func testFilterWhiteDeltaDifference() throws { 80 | var colors = [ColorFrequency]() 81 | // Create colors with brightness from 0 to 10 82 | for lightness in stride(from: 0, to: 1.1, by: 0.1) { 83 | let cgColor = CGColor(srgbRed: lightness, green: lightness, blue: lightness, alpha: 1.0) 84 | colors.append(ColorFrequency(color: cgColor, count: 1)) 85 | } 86 | XCTAssertEqual(colors.count, 11) 87 | 88 | let whiteColors = DominantColors.removeWhite(delta: Self.blackMaxDeltaCIE76, colors: &colors, using: .CIE76).map { $0.color } 89 | XCTAssertEqual(colors.count, 9) 90 | 91 | let white = CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1) 92 | XCTAssertTrue(whiteColors.contains(white)) 93 | 94 | let almostWhite = CGColor(srgbRed: 0.9, green: 0.9, blue: 0.9, alpha: 1) 95 | XCTAssertTrue(whiteColors.contains(almostWhite)) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/FilterImageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilterImageTests.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 26.04.2024. 6 | // 7 | 8 | import XCTest 9 | @testable import DominantColors 10 | 11 | #if os(OSX) 12 | final class FilterImageTests: XCTestCase { 13 | 14 | func testPixellate() throws { 15 | let name = NSImage.Name("Pixeleate_Image") 16 | let nsImage = Bundle.module.image(forResource: name) 17 | let cgImage = nsImage!.cgImage(forProposedRect: nil, context: nil, hints: nil)! 18 | 19 | let quality: DominantColorQuality = .fair 20 | let pixellateImage = try ImageFilter.pixellate(image: cgImage, by: quality) 21 | 22 | let size = CGSize(width: pixellateImage.width, height: pixellateImage.height) 23 | let expectationSize = CGSize(width: 96, height: 96) 24 | XCTAssertEqual(size, expectationSize) 25 | } 26 | 27 | func testCropAlpha() throws { 28 | let name = NSImage.Name("Test_Crop_Alpha") 29 | let nsImage = Bundle.module.image(forResource: name) 30 | let cgImage = nsImage!.cgImage(forProposedRect: nil, context: nil, hints: nil)! 31 | 32 | let croppedImage = try ImageFilter.cropAlpha(image: cgImage) 33 | 34 | let size = CGSize(width: croppedImage.width, height: croppedImage.height) 35 | let expectationSize = CGSize(width: 32, height: 32) 36 | XCTAssertEqual(size, expectationSize) 37 | 38 | guard let cfData = croppedImage.dataProvider!.data, 39 | let data = CFDataGetBytePtr(cfData) 40 | else { throw ImageColorError.cgImageDataFailure } 41 | 42 | var colorsOnImage: [CGColor] = [] 43 | for yCoordonate in 0 ..< croppedImage.height { 44 | for xCoordonate in 0 ..< croppedImage.width { 45 | let index = (croppedImage.width * yCoordonate + xCoordonate) * 4 46 | 47 | let alpha = CGFloat(data[index + 3]) 48 | let red = CGFloat(data[index + 0]) 49 | let green = CGFloat(data[index + 1]) 50 | let blue = CGFloat(data[index + 2]) 51 | 52 | let pixelColor = CGColor(srgbRed: red, green: green, blue: blue, alpha: alpha) 53 | colorsOnImage.append(pixelColor) 54 | } 55 | } 56 | 57 | let alphaColors = colorsOnImage.filter({ $0.alpha == 0 }) 58 | XCTAssert(alphaColors.isEmpty) 59 | } 60 | } 61 | #endif 62 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/GradientColorsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientColorsTests.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 03.12.2023. 6 | // 7 | 8 | import XCTest 9 | @testable import DominantColors 10 | 11 | final class GradientColorsTests: XCTestCase { 12 | 13 | func testColorsAtEdgesPercentage() throws { 14 | let colors: [CGColor] = [ 15 | .init(red: 0, green: 0, blue: 0, alpha: 1), 16 | .init(red: 1, green: 0, blue: 0, alpha: 1) 17 | ] 18 | 19 | let colorAtStart = colors.gradientColor(percent: 0) 20 | let colorAtFinal = colors.gradientColor(percent: 100) 21 | 22 | XCTAssertEqual(colorAtStart, colors.first) 23 | XCTAssertEqual(colorAtFinal, colors.last) 24 | } 25 | 26 | func testColorsAtEdgesSize() throws { 27 | let colors: [CGColor] = [ 28 | .init(red: 0, green: 1, blue: 0, alpha: 1), 29 | .init(red: 0, green: 0, blue: 0, alpha: 1) 30 | ] 31 | 32 | let colorAtStart = colors.gradientColor(at: 1, size: 3) 33 | let colorAtFinal = colors.gradientColor(at: 3, size: 3) 34 | 35 | XCTAssertEqual(colorAtStart, colors.first) 36 | XCTAssertEqual(colorAtFinal, colors.last) 37 | } 38 | 39 | func testColorAtMiddlePercentage() throws { 40 | let colors: [CGColor] = [ 41 | .init(red: 1, green: 0, blue: 0, alpha: 1), 42 | .init(red: 0, green: 0, blue: 1, alpha: 1) 43 | ] 44 | 45 | let colorMiddle = colors.gradientColor(percent: 50) 46 | 47 | let colorMiddleExpectation = CGColor(red: 0.5, green: 0, blue: 0.5, alpha: 1) 48 | 49 | XCTAssertEqual(colorMiddle, colorMiddleExpectation) 50 | } 51 | 52 | func testColorAtMiddleSize() throws { 53 | let colors: [CGColor] = [ 54 | .init(red: 0, green: 1, blue: 0, alpha: 1), 55 | .init(red: 1, green: 0, blue: 1, alpha: 1) 56 | ] 57 | 58 | let colorMiddle = colors.gradientColor(at: 2, size: 3) 59 | 60 | let colorMiddleExpectation = CGColor(red: 0.5, green: 0.5, blue: 0.5, alpha: 1) 61 | 62 | XCTAssertEqual(colorMiddle, colorMiddleExpectation) 63 | } 64 | 65 | func testGradientColors() throws { 66 | let colors: [CGColor] = [ 67 | .init(red: 0, green: 0, blue: 1, alpha: 1), 68 | .init(red: 1, green: 0, blue: 0, alpha: 1) 69 | ] 70 | let size: CGFloat = 11 71 | 72 | let gradientColorsExpectation: [CGColor] = [ 73 | .init(red: 0.0, green: 0, blue: 1.0, alpha: 1), 74 | .init(red: 0.1, green: 0, blue: 0.9, alpha: 1), 75 | .init(red: 0.2, green: 0, blue: 0.8, alpha: 1), 76 | .init(red: 0.3, green: 0, blue: 0.7, alpha: 1), 77 | .init(red: 0.4, green: 0, blue: 0.6, alpha: 1), 78 | .init(red: 0.5, green: 0, blue: 0.5, alpha: 1), 79 | .init(red: 0.6, green: 0, blue: 0.4, alpha: 1), 80 | .init(red: 0.7, green: 0, blue: 0.3, alpha: 1), 81 | .init(red: 0.8, green: 0, blue: 0.2, alpha: 1), 82 | .init(red: 0.9, green: 0, blue: 0.1, alpha: 1), 83 | .init(red: 1.0, green: 0, blue: 0.0, alpha: 1) 84 | ] 85 | 86 | let gradientColors = colors.gradientColors(in: size) 87 | 88 | XCTAssertEqual(gradientColors.count, gradientColorsExpectation.count) 89 | 90 | XCTAssertEqual(gradientColors.first, colors.first) 91 | XCTAssertEqual(gradientColors.last, colors.last) 92 | 93 | let accuracy = 0.000000001 94 | gradientColors.enumerated().forEach { index, color in 95 | color.components?.enumerated().forEach({ componentIndex, value in 96 | let valueExpectation = gradientColorsExpectation[index].components![componentIndex] 97 | XCTAssertEqual(value, valueExpectation, accuracy: accuracy) 98 | }) 99 | } 100 | } 101 | 102 | func testMultiColors() { 103 | let colors: [CGColor] = [ 104 | .init(red: 1, green: 0, blue: 0, alpha: 1), 105 | .init(red: 0, green: 1, blue: 0, alpha: 1), 106 | .init(red: 0, green: 0, blue: 1, alpha: 1) 107 | ] 108 | 109 | let gradientColors = colors.gradientColors(in: 5) 110 | 111 | let gradientColorsExpectation: [CGColor] = [ 112 | .init(red: 1.0, green: 0.0, blue: 0.0, alpha: 1), 113 | .init(red: 0.5, green: 0.5, blue: 0.0, alpha: 1), 114 | .init(red: 0.0, green: 1.0, blue: 0.0, alpha: 1), 115 | .init(red: 0.0, green: 0.5, blue: 0.5, alpha: 1), 116 | .init(red: 0.0, green: 0.0, blue: 1.0, alpha: 1) 117 | ] 118 | 119 | let accuracy = 0.000000001 120 | gradientColors.enumerated().forEach { index, color in 121 | color.components?.enumerated().forEach({ componentIndex, value in 122 | let valueExpectation = gradientColorsExpectation[index].components![componentIndex] 123 | XCTAssertEqual(value, valueExpectation, accuracy: accuracy) 124 | }) 125 | } 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/HSLTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HSLTests.swift 3 | // 4 | // 5 | // Created by Denis Dmitriev on 05.09.2023. 6 | // 7 | 8 | import XCTest 9 | @testable import DominantColors 10 | 11 | class HSLTests: XCTestCase { 12 | 13 | /// https://convertacolor.com for check results 14 | 15 | func testRed() { 16 | let color = CGColor(red: 1, green: .zero, blue: .zero, alpha: 1) 17 | 18 | XCTAssertEqual(color.hue, 0) 19 | XCTAssertEqual(color.saturation, 100) 20 | XCTAssertEqual(color.lightness, 50) 21 | } 22 | 23 | func testGreen() { 24 | let color = CGColor(red: .zero, green: 1, blue: .zero, alpha: 1) 25 | 26 | XCTAssertEqual(color.hue, 120) 27 | XCTAssertEqual(color.saturation, 100) 28 | XCTAssertEqual(color.lightness, 50) 29 | } 30 | 31 | func testBlue() { 32 | let color = CGColor(red: .zero, green: .zero, blue: 1, alpha: 1) 33 | 34 | XCTAssertEqual(color.hue, 240) 35 | XCTAssertEqual(color.saturation, 100) 36 | XCTAssertEqual(color.lightness, 50) 37 | } 38 | 39 | func testWhite() { 40 | let color = CGColor(red: 1, green: 1, blue: 1, alpha: 1) 41 | 42 | XCTAssertEqual(color.hue, 0) 43 | XCTAssertEqual(color.saturation, 0) 44 | XCTAssertEqual(color.lightness, 100) 45 | } 46 | 47 | func testBlack() { 48 | let color = CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 1.0) 49 | 50 | XCTAssertEqual(color.hue, 0) 51 | XCTAssertEqual(color.saturation, 0) 52 | XCTAssertEqual(color.lightness, 0) 53 | } 54 | 55 | func testArbitrary() { 56 | let color = CGColor(red: 129.0 / 255.0, green: 200.0 / 255.0, blue: 10.0 / 255.0, alpha: 1.0) 57 | 58 | let roundHue = color.hue.rounded(.toNearestOrAwayFromZero, precision: 10) 59 | let roundLightness = color.lightness.rounded(.toNearestOrAwayFromZero, precision: 10) 60 | let roundSaturation = color.saturation.rounded(.toNearestOrAwayFromZero, precision: 10) 61 | 62 | XCTAssertEqual(roundHue, 82.4) 63 | XCTAssertEqual(roundSaturation, 90.5) 64 | XCTAssertEqual(roundLightness, 41.2) 65 | } 66 | 67 | func testHSLToCGColor() { 68 | let white = HSL(hue: 0, saturation: 100, lightness: 100) 69 | XCTAssertEqual(white.cgColor, CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)) 70 | 71 | let gray = HSL(hue: 120, saturation: 0, lightness: 50) 72 | XCTAssertEqual(gray.cgColor, CGColor(srgbRed: 50 / 100, green: 50 / 100, blue: 50 / 100, alpha: 1.0)) 73 | 74 | let black = HSL(hue: 240, saturation: 75, lightness: 0) 75 | XCTAssertEqual(black.cgColor, CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 1)) 76 | 77 | let red = HSL(hue: 0, saturation: 100, lightness: 50) 78 | XCTAssertEqual(red.cgColor, CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1.0)) 79 | 80 | let green = HSL(hue: 120, saturation: 100, lightness: 50) 81 | XCTAssertEqual(green.cgColor, CGColor(srgbRed: 0, green: 1, blue: 0, alpha: 1.0)) 82 | 83 | let blue = HSL(hue: 240, saturation: 100, lightness: 50) 84 | XCTAssertEqual(blue.cgColor, CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 1.0)) 85 | 86 | let someYellow = HSL(hue: 47, saturation: 83, lightness: 53) 87 | let expectationSomeYellow = CGColor(srgbRed: 235 / 255, green: 190 / 255, blue: 33 / 255, alpha: 1.0) 88 | XCTAssertEqual(someYellow.cgColor.red255, expectationSomeYellow.red255, accuracy: 2) 89 | XCTAssertEqual(someYellow.cgColor.green255, expectationSomeYellow.green255, accuracy: 2) 90 | XCTAssertEqual(someYellow.cgColor.blue255, expectationSomeYellow.blue255, accuracy: 3) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/HexTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HexTests.swift 3 | // ColorKitTests 4 | // 5 | // Created by Boris Emorine on 2/27/20. 6 | // Copyright © 2020 BorisEmorine. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import DominantColors 11 | 12 | class HexTests: XCTestCase { 13 | 14 | private let blackHex = "#000000" 15 | private let whiteHex = "#ffffff" 16 | private let redHex = "#ff0000" 17 | private let darkGreen = "#32a852" 18 | private let lightGreen = "#43ff64d9" 19 | 20 | // Init 21 | 22 | func testInitBlack() { 23 | let hexColor = Hex(hex: blackHex)! 24 | XCTAssertEqual(hexColor.cgColor.red, 0) 25 | XCTAssertEqual(hexColor.cgColor.green, 0) 26 | XCTAssertEqual(hexColor.cgColor.blue, 0) 27 | XCTAssertEqual(hexColor.cgColor.alpha, 1) 28 | } 29 | 30 | func testInitWhite() { 31 | let hexColor = Hex(hex: whiteHex)! 32 | XCTAssertEqual(hexColor.cgColor.red, 1) 33 | XCTAssertEqual(hexColor.cgColor.green, 1) 34 | XCTAssertEqual(hexColor.cgColor.blue, 1) 35 | XCTAssertEqual(hexColor.cgColor.alpha, 1) 36 | } 37 | 38 | func testInitRed() { 39 | let hexColor = Hex(hex: redHex)! 40 | XCTAssertEqual(hexColor.cgColor.red, 255.0 / 255.0) 41 | XCTAssertEqual(hexColor.cgColor.green, 0.0 / 255.0) 42 | XCTAssertEqual(hexColor.cgColor.blue, 0.0 / 255.0) 43 | XCTAssertEqual(hexColor.cgColor.alpha, 1) 44 | } 45 | 46 | func testInitDarkGreen() { 47 | let hexColor = Hex(hex: darkGreen)! 48 | XCTAssertEqual(hexColor.cgColor.red, 50.0 / 255.0) 49 | XCTAssertEqual(hexColor.cgColor.green, 168.0 / 255.0) 50 | XCTAssertEqual(hexColor.cgColor.blue, 82.0 / 255.0) 51 | XCTAssertEqual(hexColor.cgColor.alpha, 1) 52 | } 53 | 54 | func testInitLightGreen() { 55 | let hexColor = Hex(hex: lightGreen)! 56 | XCTAssertEqual(hexColor.cgColor.red, 67.0 / 255.0) 57 | XCTAssertEqual(hexColor.cgColor.green, 255.0 / 255.0) 58 | XCTAssertEqual(hexColor.cgColor.blue, 100.0 / 255.0) 59 | XCTAssertEqual(hexColor.cgColor.alpha, 0.85, accuracy: 0.001) 60 | } 61 | 62 | // hex 63 | 64 | func testHexBlack() { 65 | let color = CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 1) 66 | let hex = Hex(cgColor: color) 67 | XCTAssertEqual(hex.hex, blackHex) 68 | } 69 | 70 | func testHexWhite() { 71 | let color = CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1) 72 | let hex = Hex(cgColor: color) 73 | XCTAssertEqual(hex.hex, whiteHex) 74 | } 75 | 76 | func testHexRed() { 77 | let color = CGColor(red: 1, green: .zero, blue: .zero, alpha: 1) 78 | let hex = Hex(cgColor: color) 79 | XCTAssertEqual(hex.hex, redHex) 80 | } 81 | 82 | func testHexDarkGreen() { 83 | let color = CGColor(red: 50.0 / 255.0, green: 168.0 / 255.0, blue: 82.0 / 255.0, alpha: 1.0) 84 | let hex = Hex(cgColor: color) 85 | XCTAssertEqual(hex.hex, darkGreen) 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/LabTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabTests.swift 3 | // ColorKitTests 4 | // 5 | // Created by Boris Emorine on 2/26/20. 6 | // Copyright © 2020 BorisEmorine. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | /// https://cielab.xyz/colorconv/ 12 | class LabTests: XCTestCase { 13 | 14 | func testRed() { 15 | let color = CGColor(red: 1, green: .zero, blue: .zero, alpha: 1) 16 | 17 | XCTAssertEqual(color.L, 53.2408) 18 | XCTAssertEqual(color.a, 80.0923) 19 | XCTAssertEqual(color.b, 67.2031) 20 | } 21 | 22 | func testGreen() { 23 | let color = CGColor(red: .zero, green: 1, blue: .zero, alpha: 1) 24 | 25 | XCTAssertEqual(color.L, 87.7347) 26 | XCTAssertEqual(color.a, -86.1827) 27 | XCTAssertEqual(color.b, 83.1793) 28 | } 29 | 30 | func testBlue() { 31 | let color = CGColor(red: .zero, green: .zero, blue: 1, alpha: 1) 32 | 33 | XCTAssertEqual(color.L, 32.2970) 34 | XCTAssertEqual(color.a, 79.1878) 35 | XCTAssertEqual(color.b, -107.8602) 36 | } 37 | 38 | func testWhite() { 39 | let color = CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1) 40 | 41 | XCTAssertEqual(color.L, 100) 42 | XCTAssertEqual(color.a, 0) 43 | XCTAssertEqual(color.b, 0) 44 | } 45 | 46 | func testBlack() { 47 | let color = CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 1) 48 | 49 | XCTAssertEqual(color.L, 0) 50 | XCTAssertEqual(color.a, 0) 51 | XCTAssertEqual(color.b, 0) 52 | } 53 | 54 | func testArbitrary() { 55 | let color = CGColor(red: 235 / 255.0, green: 87 / 255.0, blue: 87 / 255.0, alpha: 1.0) 56 | 57 | XCTAssertEqual(color.L, 57.2426) 58 | XCTAssertEqual(color.a, 57.0894) 59 | XCTAssertEqual(color.b, 30.9286) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/BlackGrayColorSpace.imageset/BlackGrayColorSpace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/BlackGrayColorSpace.imageset/BlackGrayColorSpace.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/BlackGrayColorSpace.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "BlackGrayColorSpace.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Black_Shades.imageset/Black_Shades.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Black_Shades.imageset/Black_Shades.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Black_Shades.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Black_Shades.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Black_White_Red_Green_Blue_Grey.imageset/Black_White_Red_Green_Blue_Grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Black_White_Red_Green_Blue_Grey.imageset/Black_White_Red_Green_Blue_Grey.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Black_White_Red_Green_Blue_Grey.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Black_White_Red_Green_Blue_Grey.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Black_White_Square.imageset/Black_White_Square.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Black_White_Square.imageset/Black_White_Square.jpg -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Black_White_Square.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Black_White_Square.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Blue_Green_Square.imageset/Blue_Green_Square.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Blue_Green_Square.imageset/Blue_Green_Square.jpg -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Blue_Green_Square.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Blue_Green_Square.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Blue_Shades.imageset/Color_icon_blue.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Blue_Shades.imageset/Color_icon_blue.svg.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Blue_Shades.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Color_icon_blue.svg.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Blue_Square_1x1.imageset/Blue_Square_1x1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Blue_Square_1x1.imageset/Blue_Square_1x1.jpg -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Blue_Square_1x1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Blue_Square_1x1.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Brown_Shades.imageset/Brown_Shades.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Brown_Shades.imageset/Brown_Shades.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Brown_Shades.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Brown_Shades.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/GrayColorSpaceImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "GrayColorSpaceImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/GrayColorSpaceImage.imageset/GrayColorSpaceImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/GrayColorSpaceImage.imageset/GrayColorSpaceImage.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Green_Shades.imageset/Color_icon_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Green_Shades.imageset/Color_icon_green.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Green_Shades.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Color_icon_green.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Green_Square.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Green_Square.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Green_Square.imageset/Green_Square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Green_Square.imageset/Green_Square.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/LittleMissSunshine.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LittleMissSunshine.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/LittleMissSunshine.imageset/LittleMissSunshine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/LittleMissSunshine.imageset/LittleMissSunshine.jpg -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Orange_Shades.imageset/Color_icon_orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Orange_Shades.imageset/Color_icon_orange.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Orange_Shades.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Color_icon_orange.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Pink_Shades.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Pink_Shades.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Pink_Shades.imageset/Pink_Shades.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Pink_Shades.imageset/Pink_Shades.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Pixeleate_Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "TestPixeleate.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Pixeleate_Image.imageset/TestPixeleate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Pixeleate_Image.imageset/TestPixeleate.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Purple_Square.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Purple_Square.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Purple_Square.imageset/Purple_Square.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Purple_Square.imageset/Purple_Square.jpg -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Red_Green_Blue.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Red_Green_Blue.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Red_Green_Blue.imageset/Red_Green_Blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Red_Green_Blue.imageset/Red_Green_Blue.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Red_Green_Blue_Black_Mini.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Red_Green_Blue_Black_Mini.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Red_Green_Blue_Black_Mini.imageset/Red_Green_Blue_Black_Mini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Red_Green_Blue_Black_Mini.imageset/Red_Green_Blue_Black_Mini.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Red_Green_Blue_Random.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Red_Green_Blue_Random.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Red_Green_Blue_Random.imageset/Red_Green_Blue_Random.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Red_Green_Blue_Random.imageset/Red_Green_Blue_Random.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Red_Green_Blue_Random_Mini.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Red_Green_Blue_Random_Mini.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Red_Green_Blue_Random_Mini.imageset/Red_Green_Blue_Random_Mini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Red_Green_Blue_Random_Mini.imageset/Red_Green_Blue_Random_Mini.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Red_Shades.imageset/Color_icon_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Red_Shades.imageset/Color_icon_red.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Red_Shades.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Color_icon_red.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/ShadesGrayColorSpace.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ShadesGrayColorSpace.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/ShadesGrayColorSpace.imageset/ShadesGrayColorSpace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/ShadesGrayColorSpace.imageset/ShadesGrayColorSpace.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Test_Crop_Alpha.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "TestCropAlpha.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Test_Crop_Alpha.imageset/TestCropAlpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Test_Crop_Alpha.imageset/TestCropAlpha.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Test_Image_1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Test_Image_1.jpeg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Test_Image_1.imageset/Test_Image_1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Test_Image_1.imageset/Test_Image_1.jpeg -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Test_Image_2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Test_Image_2.jpeg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Test_Image_2.imageset/Test_Image_2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Test_Image_2.imageset/Test_Image_2.jpeg -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Test_Image_3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Test_Image_3.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Test_Image_3.imageset/Test_Image_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Test_Image_3.imageset/Test_Image_3.jpg -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Violet_Shades.imageset/Color_icon_violet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Violet_Shades.imageset/Color_icon_violet.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Violet_Shades.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Color_icon_violet.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/WaterLife1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "WaterLife1.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/WaterLife1.imageset/WaterLife1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/WaterLife1.imageset/WaterLife1.jpg -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Yellow_Shades.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Yellow_Shades.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/Media.xcassets/Yellow_Shades.imageset/Yellow_Shades.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenDmitriev/DominantColors/a05327d36092c47e9ddc77ce565eb35a5b4a4a6e/Tests/DominantColorsTests/Media.xcassets/Yellow_Shades.imageset/Yellow_Shades.png -------------------------------------------------------------------------------- /Tests/DominantColorsTests/RGBTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RGBTests.swift 3 | // ColorKitTests 4 | // 5 | // Created by Boris Emorine on 2/24/20. 6 | // Copyright © 2020 BorisEmorine. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import DominantColors 11 | 12 | class RGBTests: XCTestCase { 13 | 14 | func testRed() { 15 | let red = CIColor.red 16 | XCTAssertEqual(red.red, 1.0) 17 | XCTAssertEqual(red.green, 0.0) 18 | XCTAssertEqual(red.blue, 0.0) 19 | XCTAssertEqual(red.alpha, 1.0) 20 | } 21 | 22 | func testGreen() { 23 | let green = CIColor.green 24 | XCTAssertEqual(green.red, 0.0) 25 | XCTAssertEqual(green.green, 1.0) 26 | XCTAssertEqual(green.blue, 0.0) 27 | XCTAssertEqual(green.alpha, 1.0) 28 | } 29 | 30 | func testBlue() { 31 | let blue = CIColor.blue 32 | XCTAssertEqual(blue.red, 0.0) 33 | XCTAssertEqual(blue.green, 0.0) 34 | XCTAssertEqual(blue.blue, 1.0) 35 | XCTAssertEqual(blue.alpha, 1.0) 36 | } 37 | 38 | func testWhite() { 39 | let blue = CIColor.white 40 | XCTAssertEqual(blue.red, 1.0) 41 | XCTAssertEqual(blue.green, 1.0) 42 | XCTAssertEqual(blue.blue, 1.0) 43 | XCTAssertEqual(blue.alpha, 1.0) 44 | } 45 | 46 | func testBlack() { 47 | let blue = CIColor.black 48 | XCTAssertEqual(blue.red, 0.0) 49 | XCTAssertEqual(blue.green, 0.0) 50 | XCTAssertEqual(blue.blue, 0.0) 51 | XCTAssertEqual(blue.alpha, 1.0) 52 | } 53 | 54 | func testGray() { 55 | let blue = CIColor.gray 56 | XCTAssertEqual(blue.red, 0.5) 57 | XCTAssertEqual(blue.green, 0.5) 58 | XCTAssertEqual(blue.blue, 0.5) 59 | XCTAssertEqual(blue.alpha, 1.0) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/RelativeLuminanceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RelativeLuminanceTests.swift 3 | // ColorKitTests 4 | // 5 | // Created by Boris Emorine on 3/13/20. 6 | // Copyright © 2020 BorisEmorine. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import DominantColors 11 | 12 | class RelativeLuminanceTests: XCTestCase { 13 | 14 | func testWhite() { 15 | let white = CGColor(colorSpace: CGColorSpace(name: CGColorSpace.sRGB)!, components: [1, 1, 1, 1])! 16 | XCTAssertEqual(white.relativeLuminance, 1.0) 17 | } 18 | 19 | func testBlack() { 20 | let black = CGColor(colorSpace: CGColorSpace(name: CGColorSpace.sRGB)!, components: [0, 0, 0, 1])! 21 | XCTAssertEqual(black.relativeLuminance, 0.0) 22 | } 23 | 24 | func testOrange() { 25 | let orange = CGColor(colorSpace: CGColorSpace(name: CGColorSpace.sRGB)!, components: [98.0 / 255.0, 44.0 / 255.0, 8.0 / 255.0, 1.0])! 26 | XCTAssertEqual(orange.relativeLuminance, 0.044) 27 | } 28 | 29 | func testPurple() { 30 | let purple = CGColor(colorSpace: CGColorSpace(name: CGColorSpace.sRGB)!, components: [120 / 255.0, 90.0 / 255.0, 200.0 / 255.0, 1.0])! 31 | XCTAssertEqual(purple.relativeLuminance, 0.155) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Tests/DominantColorsTests/XYZTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XYZTests.swift 3 | // ColorKitTests 4 | // 5 | // Created by Boris Emorine on 2/24/20. 6 | // Copyright © 2020 BorisEmorine. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import DominantColors 11 | 12 | /// http://www.brucelindbloom.com/index.html?ColorCalculator.html 13 | class XYZTests: XCTestCase { 14 | 15 | func testRed() { 16 | let red = CGColor(colorSpace: CGColorSpace(name: CGColorSpace.sRGB)!, components: [1, 0, 0, 1])! 17 | 18 | XCTAssertEqual(red.X, 41.2456) 19 | XCTAssertEqual(red.Y, 21.2673) 20 | XCTAssertEqual(red.Z, 1.9334) 21 | } 22 | 23 | func testGreen() { 24 | let green = CGColor(colorSpace: CGColorSpace(name: CGColorSpace.sRGB)!, components: [0, 1, 0, 1])! 25 | 26 | XCTAssertEqual(green.X, 35.7576) 27 | XCTAssertEqual(green.Y, 71.5152) 28 | XCTAssertEqual(green.Z, 11.9192) 29 | } 30 | 31 | func testBlue() { 32 | let blue = CGColor(colorSpace: CGColorSpace(name: CGColorSpace.sRGB)!, components: [0, 0, 1, 1])! 33 | 34 | XCTAssertEqual(blue.X, 18.0438) 35 | XCTAssertEqual(blue.Y, 7.2175) 36 | XCTAssertEqual(blue.Z, 95.0304) 37 | } 38 | 39 | func testWhite() { 40 | let white = CGColor(colorSpace: CGColorSpace(name: CGColorSpace.sRGB)!, components: [1, 1, 1, 1])! 41 | 42 | XCTAssertEqual(white.X, 95.0470) 43 | XCTAssertEqual(white.Y, 100) 44 | XCTAssertEqual(white.Z, 108.8830) 45 | } 46 | 47 | func testBlack() { 48 | let black = CGColor(colorSpace: CGColorSpace(name: CGColorSpace.sRGB)!, components: [0, 0, 0, 1])! 49 | 50 | XCTAssertEqual(black.X, 0) 51 | XCTAssertEqual(black.Y, 0) 52 | XCTAssertEqual(black.Z, 0) 53 | } 54 | 55 | func testArbitrary() { 56 | let color = CGColor(colorSpace: CGColorSpace(name: CGColorSpace.sRGB)!, components: [129.0 / 255.0, 200.0 / 255.0, 10.0 / 255.0, 1])! 57 | 58 | XCTAssertEqual(color.X, 29.7622) 59 | XCTAssertEqual(color.Y, 45.9964) 60 | XCTAssertEqual(color.Z, 7.5972) 61 | } 62 | 63 | } 64 | --------------------------------------------------------------------------------