├── .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 |
--------------------------------------------------------------------------------