camalian 0.0.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/gpr-push.yml +30 -0
- data/.github/workflows/ruby.yml +32 -0
- data/.github/workflows/rubygems-push.yml +29 -0
- data/.gitignore +2 -1
- data/.overcommit.yml +33 -0
- data/.rubocop.yml +37 -0
- data/.tool-versions +1 -0
- data/.travis.yml +17 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +0 -0
- data/README.md +46 -3
- data/Rakefile +13 -1
- data/camalian.gemspec +18 -11
- data/lib/camalian.rb +30 -3
- data/lib/camalian/color.rb +111 -0
- data/lib/camalian/image.rb +38 -0
- data/lib/camalian/palette.rb +46 -0
- data/lib/camalian/quantization/histogram.rb +26 -0
- data/lib/camalian/quantization/k_means.rb +59 -0
- data/lib/camalian/quantization/median_cut.rb +74 -0
- data/lib/camalian/version.rb +3 -1
- data/test/assets/palette.png +0 -0
- data/test/color_test.rb +32 -0
- data/test/palette_test.rb +64 -0
- data/test/quantization/histogram_test.rb +61 -0
- data/test/quantization/k_means_test.rb +61 -0
- data/test/quantization/median_cut_test.rb +63 -0
- data/test/test_helper.rb +11 -0
- metadata +99 -15
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d6d9583383ecf60a0cd259bcce471abe319f2b4390b52c53d63a2504f3dd020f
|
4
|
+
data.tar.gz: 6e5cb3a2c174539b91bae01fafad0e9ef658756bebc7809837cc0edb019cfacd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c79fb3ea3e499fee16fa36f8e10c640a54a1ddc3d0121a99d5053b7c28a60cfbe903f2600c59ec61997a42942f40b16298714904dfbbdb170dbbfa0adf23540b
|
7
|
+
data.tar.gz: 4d7a82621d0429c005f4d3743724bc2de51c7f57110416d50a1397de8d6a473e73ec25416f53533b12c500670709412cee6fd73b529fc68dfc044e5112f54578
|
@@ -0,0 +1,30 @@
|
|
1
|
+
name: Github Packages Push
|
2
|
+
|
3
|
+
on:
|
4
|
+
workflow_dispatch:
|
5
|
+
push:
|
6
|
+
tags:
|
7
|
+
- '*'
|
8
|
+
jobs:
|
9
|
+
build:
|
10
|
+
name: Build + Publish
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
|
13
|
+
steps:
|
14
|
+
- uses: actions/checkout@v2
|
15
|
+
- name: Set up Ruby 2.6
|
16
|
+
uses: actions/setup-ruby@v1
|
17
|
+
with:
|
18
|
+
ruby-version: 2.6.x
|
19
|
+
|
20
|
+
- name: Publish to GPR
|
21
|
+
run: |
|
22
|
+
mkdir -p $HOME/.gem
|
23
|
+
touch $HOME/.gem/credentials
|
24
|
+
chmod 0600 $HOME/.gem/credentials
|
25
|
+
printf -- "---\n:github: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
26
|
+
gem build *.gemspec
|
27
|
+
gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
|
28
|
+
env:
|
29
|
+
GEM_HOST_API_KEY: "Bearer ${{secrets.GITHUB_TOKEN}}"
|
30
|
+
OWNER: ${{ github.repository_owner }}
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# This workflow uses actions that are not certified by GitHub.
|
2
|
+
# They are provided by a third-party and are governed by
|
3
|
+
# separate terms of service, privacy policy, and support
|
4
|
+
# documentation.
|
5
|
+
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
|
6
|
+
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
|
7
|
+
|
8
|
+
name: build
|
9
|
+
|
10
|
+
on:
|
11
|
+
push:
|
12
|
+
branches: [ master ]
|
13
|
+
pull_request:
|
14
|
+
branches: [ master ]
|
15
|
+
|
16
|
+
jobs:
|
17
|
+
test:
|
18
|
+
runs-on: ubuntu-16.04
|
19
|
+
strategy:
|
20
|
+
matrix:
|
21
|
+
ruby: [ '2.4', '2.5', '2.6', '2.7' ]
|
22
|
+
name: Ruby ${{ matrix.ruby }}
|
23
|
+
steps:
|
24
|
+
- uses: actions/checkout@v2
|
25
|
+
- uses: actions/setup-ruby@v1
|
26
|
+
with:
|
27
|
+
ruby-version: ${{ matrix.ruby }}
|
28
|
+
- name: Install dependencies
|
29
|
+
run: bundle install
|
30
|
+
- name: Run tests
|
31
|
+
run: bundle exec rake
|
32
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
name: Ruby Gems Push
|
2
|
+
|
3
|
+
on:
|
4
|
+
workflow_dispatch:
|
5
|
+
push:
|
6
|
+
tags:
|
7
|
+
- '*'
|
8
|
+
jobs:
|
9
|
+
build:
|
10
|
+
name: Build + Publish
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
|
13
|
+
steps:
|
14
|
+
- uses: actions/checkout@v2
|
15
|
+
- name: Set up Ruby 2.6
|
16
|
+
uses: actions/setup-ruby@v1
|
17
|
+
with:
|
18
|
+
ruby-version: 2.6.x
|
19
|
+
|
20
|
+
- name: Publish to RubyGems
|
21
|
+
run: |
|
22
|
+
mkdir -p $HOME/.gem
|
23
|
+
touch $HOME/.gem/credentials
|
24
|
+
chmod 0600 $HOME/.gem/credentials
|
25
|
+
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
26
|
+
gem build *.gemspec
|
27
|
+
gem push *.gem
|
28
|
+
env:
|
29
|
+
GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
|
data/.gitignore
CHANGED
data/.overcommit.yml
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# Use this file to configure the Overcommit hooks you wish to use. This will
|
2
|
+
# extend the default configuration defined in:
|
3
|
+
# https://github.com/sds/overcommit/blob/master/config/default.yml
|
4
|
+
#
|
5
|
+
# At the topmost level of this YAML file is a key representing type of hook
|
6
|
+
# being run (e.g. pre-commit, commit-msg, etc.). Within each type you can
|
7
|
+
# customize each hook, such as whether to only run it on certain files (via
|
8
|
+
# `include`), whether to only display output if it fails (via `quiet`), etc.
|
9
|
+
#
|
10
|
+
# For a complete list of hooks, see:
|
11
|
+
# https://github.com/sds/overcommit/tree/master/lib/overcommit/hook
|
12
|
+
#
|
13
|
+
# For a complete list of options that you can use to customize hooks, see:
|
14
|
+
# https://github.com/sds/overcommit#configuration
|
15
|
+
#
|
16
|
+
# Uncomment the following lines to make the configuration take effect.
|
17
|
+
|
18
|
+
PreCommit:
|
19
|
+
RuboCop:
|
20
|
+
enabled: true
|
21
|
+
on_warn: fail # Treat all warnings as failures
|
22
|
+
|
23
|
+
# TrailingWhitespace:
|
24
|
+
# enabled: true
|
25
|
+
# exclude:
|
26
|
+
# - '**/db/structure.sql' # Ignore trailing whitespace in generated files
|
27
|
+
#
|
28
|
+
#PostCheckout:
|
29
|
+
# ALL: # Special hook name that customizes all hooks of this type
|
30
|
+
# quiet: true # Change all post-checkout hooks to only display output on failure
|
31
|
+
#
|
32
|
+
# IndexTags:
|
33
|
+
# enabled: true # Generate a tags file with `ctags` each time HEAD changes
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# The behavior of RuboCop can be controlled via the .rubocop.yml
|
2
|
+
# configuration file. It makes it possible to enable/disable
|
3
|
+
# certain cops (checks) and to alter their behavior if they accept
|
4
|
+
# any parameters. The file can be placed either in your home
|
5
|
+
# directory or in some project directory.
|
6
|
+
#
|
7
|
+
# RuboCop will start looking for the configuration file in the directory
|
8
|
+
# where the inspected file is and continue its way up to the root directory.
|
9
|
+
#
|
10
|
+
# See https://docs.rubocop.org/rubocop/configuration
|
11
|
+
|
12
|
+
AllCops:
|
13
|
+
NewCops: enable
|
14
|
+
|
15
|
+
Naming/MethodParameterName:
|
16
|
+
AllowedNames:
|
17
|
+
- r
|
18
|
+
- g
|
19
|
+
- b
|
20
|
+
|
21
|
+
Metrics/MethodLength:
|
22
|
+
Max: 50
|
23
|
+
|
24
|
+
Metrics/PerceivedComplexity:
|
25
|
+
Max: 25
|
26
|
+
|
27
|
+
Metrics/CyclomaticComplexity:
|
28
|
+
Max: 15
|
29
|
+
|
30
|
+
Metrics/BlockLength:
|
31
|
+
Exclude:
|
32
|
+
- test/**/*
|
33
|
+
|
34
|
+
Metrics/AbcSize:
|
35
|
+
Max: 30
|
36
|
+
IgnoredMethods:
|
37
|
+
- build_components
|
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 2.7.3
|
data/.travis.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 2.7
|
4
|
+
- 2.6
|
5
|
+
- 2.5
|
6
|
+
- 2.4
|
7
|
+
|
8
|
+
before_script:
|
9
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
10
|
+
- chmod +x ./cc-test-reporter
|
11
|
+
- ./cc-test-reporter before-build
|
12
|
+
|
13
|
+
script:
|
14
|
+
- bundle exec rake test
|
15
|
+
|
16
|
+
after_script:
|
17
|
+
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
data/Gemfile
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
source 'https://rubygems.org'
|
2
4
|
|
3
5
|
# Specify your gem's dependencies in camalian.gemspec
|
4
6
|
gemspec
|
7
|
+
|
8
|
+
gem 'overcommit', require: false
|
9
|
+
gem 'rubocop', '~> 1.3', require: false
|
10
|
+
gem 'rubocop-daemon', require: false
|
11
|
+
gem 'simplecov', require: false
|
data/LICENSE.txt
CHANGED
File without changes
|
data/README.md
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
# Camalian
|
2
2
|
|
3
|
-
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/camalian.svg)](https://badge.fury.io/rb/camalian)
|
4
|
+
![Ruby](https://github.com/nazarhussain/camalian/workflows/build/badge.svg?branch=master)
|
5
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/5495a2c122469d81b6c5/maintainability)](https://codeclimate.com/github/nazarhussain/camalian/maintainability)
|
6
|
+
|
7
|
+
Ruby gem to extract color palettes from images and play with their saturation
|
4
8
|
|
5
9
|
## Installation
|
6
10
|
|
7
11
|
Add this line to your application's Gemfile:
|
8
12
|
|
9
|
-
gem 'camalian'
|
13
|
+
gem 'camalian', '~> 0.2.0'
|
10
14
|
|
11
15
|
And then execute:
|
12
16
|
|
@@ -18,7 +22,46 @@ Or install it yourself as:
|
|
18
22
|
|
19
23
|
## Usage
|
20
24
|
|
21
|
-
|
25
|
+
```ruby
|
26
|
+
image = Camalian::load('file_path')
|
27
|
+
colors = image.prominent_colors(15)
|
28
|
+
colors = colors.sort_similar_colors
|
29
|
+
colors.light_colors(0, 40)
|
30
|
+
```
|
31
|
+
|
32
|
+
You can find a working example with detail explanation and reference code here on [this link](https://basicdrift.com/color-extraction-library-build-color-search-engine-fdf369678d5a). Here we will build a functional color based image search engine in Ruby on Rails.
|
33
|
+
|
34
|
+
NOTE: Since its a compute intensive operation so for production use its suggested to use under a background job and not within a request/response cycle.
|
35
|
+
|
36
|
+
## Quantization Algorithms
|
37
|
+
|
38
|
+
Currently following algorithms are implemented.
|
39
|
+
|
40
|
+
### Histogram
|
41
|
+
|
42
|
+
Its a most common algorithm for color quantization and used different bucket technique to group the colors together. You can read more about this [technique here](https://en.wikipedia.org/wiki/Color_histogram). It can be accessed by `Camalian::QUANTIZATION_HISTOGRAM` constant. This is used as default method as well.
|
43
|
+
|
44
|
+
### K Means
|
45
|
+
|
46
|
+
This algorithm uses color distancing in RGB space to group the similar colors. You can learn more about this [technique here](https://en.wikipedia.org/wiki/K-means_clustering). It can be accessed by `Camalian::QUANTIZATION_K_MEANS` constant.
|
47
|
+
|
48
|
+
### Median Cut
|
49
|
+
|
50
|
+
This algorithm uses color highest color range to determine the median and split colors to groups. The output consists of average color of such color groups. Since these algorithm don't use actual colors and instead average, so you will may not exact matching pixel in the image. This algorithm is nice to be used with image compression, where similarity and compression is important than having same pixel colors. You can learn more about this [technique here](https://tpgit.github.io/UnOfficialLeptDocs/leptonica/color-quantization.html) . It can be accessed by `Camalian::QUANTIZATION_MEDIAN_CUT` constant.
|
51
|
+
|
52
|
+
|
53
|
+
You can set default quantization method globally as:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
Camalian.options[:quantization] = Camalian::QUANTIZATION_K_MEANS
|
57
|
+
```
|
58
|
+
|
59
|
+
or you can set at the time of extracting colors by.
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
image = Camalian::load('file_path')
|
63
|
+
colors = image.prominent_colors(15, quantization: Camalian::QUANTIZATION_K_MEANS)
|
64
|
+
```
|
22
65
|
|
23
66
|
## Contributing
|
24
67
|
|
data/Rakefile
CHANGED
@@ -1 +1,13 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
|
5
|
+
require 'rake/testtask'
|
6
|
+
|
7
|
+
Rake::TestTask.new do |t|
|
8
|
+
t.libs << 'test'
|
9
|
+
t.pattern = 'test/**/*_test.rb'
|
10
|
+
end
|
11
|
+
|
12
|
+
desc 'Run tests'
|
13
|
+
task default: :test
|
data/camalian.gemspec
CHANGED
@@ -1,21 +1,28 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require 'camalian/version'
|
5
6
|
|
6
7
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
8
|
+
spec.name = 'camalian'
|
8
9
|
spec.version = Camalian::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
11
|
-
spec.description =
|
12
|
-
spec.summary =
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
10
|
+
spec.authors = ['Nazar Hussain']
|
11
|
+
spec.email = ['nazarhussain@gmail.com']
|
12
|
+
spec.description = 'Library used to deal with colors and images'
|
13
|
+
spec.summary = 'Library used to deal with colors and images. You can extract colors from images.'
|
14
|
+
spec.homepage = 'https://github.com/nazarhussain/camalian'
|
15
|
+
spec.license = 'MIT'
|
15
16
|
|
16
|
-
spec.files = `git ls-files`.split(
|
17
|
+
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
17
18
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
19
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
-
spec.require_paths = [
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.add_dependency 'chunky_png', '~> 1.3', '>= 1.3.14'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'minitest', '~> 5.14', '>= 5.14.2'
|
25
|
+
spec.add_development_dependency 'rake', '~> 13.0', '>= 13.0.1'
|
20
26
|
|
27
|
+
spec.required_ruby_version = '>= 2.4'
|
21
28
|
end
|
data/lib/camalian.rb
CHANGED
@@ -1,5 +1,32 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require 'chunky_png'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'open-uri'
|
6
|
+
|
7
|
+
require 'camalian/version'
|
8
|
+
require 'camalian/color'
|
9
|
+
require 'camalian/palette'
|
10
|
+
require 'camalian/image'
|
11
|
+
require 'camalian/quantization/histogram'
|
12
|
+
require 'camalian/quantization/k_means'
|
13
|
+
require 'camalian/quantization/median_cut'
|
14
|
+
|
15
|
+
module Camalian # :nodoc:
|
16
|
+
QUANTIZATION_HISTOGRAM = Camalian::Quantization::Histogram
|
17
|
+
QUANTIZATION_K_MEANS = Camalian::Quantization::KMeans
|
18
|
+
QUANTIZATION_MEDIAN_CUT = Camalian::Quantization::MedianCut
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def options
|
22
|
+
@options ||= {
|
23
|
+
color_count: 8,
|
24
|
+
quantization: Camalian::QUANTIZATION_HISTOGRAM
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def load(image_path)
|
29
|
+
Image.new(image_path)
|
30
|
+
end
|
31
|
+
end
|
5
32
|
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Camalian
|
4
|
+
# Camalian color object
|
5
|
+
class Color
|
6
|
+
attr_reader :r, :g, :b, :h, :s, :l, :hsv
|
7
|
+
|
8
|
+
def initialize(r, g, b)
|
9
|
+
build_components(r, g, b)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.from_hex(hex_value)
|
13
|
+
color_hash = hex_value[0..6]
|
14
|
+
color_hash = color_hash[1..6] if color_hash[0] == '#'
|
15
|
+
r = color_hash[0..1].to_i(16)
|
16
|
+
g = color_hash[2..3].to_i(16)
|
17
|
+
b = color_hash[4..5].to_i(16)
|
18
|
+
|
19
|
+
Color.new(r, g, b)
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
"red=#{r} green=#{g} blue=#{b} hue=#{h} saturation=#{s} lightness=#{l}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_hex
|
27
|
+
"##{r.to_s(16).rjust(2, '0')}#{g.to_s(16).rjust(2, '0')}#{b.to_s(16).rjust(2, '0')}"
|
28
|
+
end
|
29
|
+
|
30
|
+
# Used for array uniqueness
|
31
|
+
def hash
|
32
|
+
"#{r}#{g}#{b}".to_i
|
33
|
+
end
|
34
|
+
|
35
|
+
# Used for object comparison
|
36
|
+
def ==(other)
|
37
|
+
r == other.r && g == other.g && b == other.b
|
38
|
+
end
|
39
|
+
alias eql? ==
|
40
|
+
|
41
|
+
def hue_distance(color)
|
42
|
+
[(h - color.h) % 360, (color.h - h) % 360].min
|
43
|
+
end
|
44
|
+
|
45
|
+
def rgb_distance(color)
|
46
|
+
Math.sqrt(((r - color.r)**2) + ((g - color.g)**2) + ((b - color.b)**2))
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def build_components(r, g, b)
|
52
|
+
@r = r
|
53
|
+
@g = g
|
54
|
+
@b = b
|
55
|
+
|
56
|
+
ri = @r / 255.0
|
57
|
+
gi = @g / 255.0
|
58
|
+
bi = @b / 255.0
|
59
|
+
|
60
|
+
cmax = [ri, gi, bi].max
|
61
|
+
cmin = [ri, gi, bi].min
|
62
|
+
delta = cmax - cmin
|
63
|
+
|
64
|
+
@l = (cmax + cmin) / 2.0
|
65
|
+
|
66
|
+
if delta.zero?
|
67
|
+
@h = 0
|
68
|
+
elsif cmax == ri
|
69
|
+
@h = 60 * (((gi - bi) / delta) % 6)
|
70
|
+
elsif cmax == gi
|
71
|
+
@h = 60 * (((bi - ri) / delta) + 2)
|
72
|
+
elsif cmax == bi
|
73
|
+
@h = 60 * (((ri - gi) / delta) + 4)
|
74
|
+
end
|
75
|
+
|
76
|
+
@s = if delta.zero?
|
77
|
+
0
|
78
|
+
else
|
79
|
+
delta / (1 - (2 * @l - 1).abs)
|
80
|
+
end
|
81
|
+
|
82
|
+
@h = @h.round(2)
|
83
|
+
@s = (@s * 100).round(2)
|
84
|
+
@l = (@l * 100).round(2)
|
85
|
+
|
86
|
+
# HSV Calculation
|
87
|
+
# Hue calculation
|
88
|
+
if delta.zero?
|
89
|
+
@hsv = [0]
|
90
|
+
elsif cmax == ri
|
91
|
+
@hsv = [60 * (((gi - bi) / delta) % 6)]
|
92
|
+
elsif cmax == gi
|
93
|
+
@hsv = [60 * (((bi - ri) / delta) + 2)]
|
94
|
+
elsif cmax == bi
|
95
|
+
@hsv = [60 * (((ri - gi) / delta) + 4)]
|
96
|
+
end
|
97
|
+
|
98
|
+
# Saturation calculation
|
99
|
+
@hsv << if cmax.zero?
|
100
|
+
0
|
101
|
+
else
|
102
|
+
delta / cmax
|
103
|
+
end
|
104
|
+
|
105
|
+
# Value calculation
|
106
|
+
@hsv << cmax
|
107
|
+
|
108
|
+
@hsv = [@hsv[0].round(2), (@hsv[1] * 100).round(2), (@hsv[2] * 100).round(2)]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Camalian
|
4
|
+
# Load image into Camalian
|
5
|
+
class Image
|
6
|
+
attr_accessor :src_file_path
|
7
|
+
|
8
|
+
def initialize(file_path)
|
9
|
+
@src_file_path = file_path
|
10
|
+
end
|
11
|
+
|
12
|
+
def prominent_colors(count = Camalian.options[:color_count],
|
13
|
+
quantization: Camalian.options[:quantization],
|
14
|
+
optimal: true)
|
15
|
+
image = ::ChunkyPNG::Image.from_file(@src_file_path)
|
16
|
+
|
17
|
+
colors = image.pixels.map do |val|
|
18
|
+
Color.new(
|
19
|
+
ChunkyPNG::Color.r(val),
|
20
|
+
ChunkyPNG::Color.g(val),
|
21
|
+
ChunkyPNG::Color.b(val)
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
quantize = quantization.new
|
26
|
+
|
27
|
+
palette = quantize.process(colors, count)
|
28
|
+
|
29
|
+
retry_count = 1
|
30
|
+
while !optimal && palette.size < count
|
31
|
+
palette = quantize.process(colors, count + retry_count)
|
32
|
+
retry_count += 1
|
33
|
+
end
|
34
|
+
|
35
|
+
palette
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Camalian
|
4
|
+
# Collection of colors with some useful features
|
5
|
+
class Palette < Array
|
6
|
+
def self.from_hex(hex_values)
|
7
|
+
new(hex_values.map { |v| Color.from_hex(v) })
|
8
|
+
end
|
9
|
+
|
10
|
+
def sort_by_lightness
|
11
|
+
Palette.new(sort_by(&:l).reverse)
|
12
|
+
end
|
13
|
+
|
14
|
+
def sort_by_hue
|
15
|
+
Palette.new(sort_by(&:h).reverse)
|
16
|
+
end
|
17
|
+
|
18
|
+
def sort_similar_colors
|
19
|
+
Palette.new(sort_by(&:hsv))
|
20
|
+
end
|
21
|
+
|
22
|
+
def sort_by_saturation
|
23
|
+
Palette.new(sort_by(&:s).reverse)
|
24
|
+
end
|
25
|
+
|
26
|
+
def average_color
|
27
|
+
r = map(&:r).inject(&:+)
|
28
|
+
g = map(&:g).inject(&:+)
|
29
|
+
b = map(&:b).inject(&:+)
|
30
|
+
size = self.size
|
31
|
+
|
32
|
+
Color.new(r / size, g / size, b / size)
|
33
|
+
end
|
34
|
+
|
35
|
+
def light_colors(limit1, limit2)
|
36
|
+
min = [limit1, limit2].min
|
37
|
+
max = [limit1, limit2].max
|
38
|
+
table = dup
|
39
|
+
Palette.new(table.delete_if { |color| color.l > max or color.l < min })
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_hex
|
43
|
+
map(&:to_hex)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Camalian
|
4
|
+
module Quantization
|
5
|
+
class Histogram # :nodoc:
|
6
|
+
def process(colors, count)
|
7
|
+
bucket_size = (255.0 / count).ceil
|
8
|
+
buckets = {}
|
9
|
+
|
10
|
+
colors.each do |color|
|
11
|
+
key = bucket_key(color, bucket_size)
|
12
|
+
buckets[key] ||= Palette.new
|
13
|
+
buckets[key].push(color)
|
14
|
+
end
|
15
|
+
|
16
|
+
Palette.new(buckets.map { |_, value| value.average_color }[0...count])
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def bucket_key(color, bucket_size)
|
22
|
+
"#{color.r / bucket_size}:#{color.g / bucket_size}:#{color.b / bucket_size}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Camalian
|
4
|
+
module Quantization
|
5
|
+
class KMeans # :nodoc:
|
6
|
+
INIT_TRIES = 10
|
7
|
+
|
8
|
+
def process(colors, count)
|
9
|
+
# Its faster to extract unique colors once
|
10
|
+
colors = colors.uniq
|
11
|
+
means = initial_means(colors, count)
|
12
|
+
|
13
|
+
done = false
|
14
|
+
|
15
|
+
until done
|
16
|
+
groups = group_by_means(colors, means)
|
17
|
+
new_means = groups.map do |group|
|
18
|
+
Palette.new(group).average_color
|
19
|
+
end
|
20
|
+
common = means & new_means
|
21
|
+
|
22
|
+
done = common.size == means.size
|
23
|
+
means = new_means
|
24
|
+
end
|
25
|
+
|
26
|
+
Palette.new(means)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def initial_means(colors, count)
|
32
|
+
count = colors.size if count > colors.size
|
33
|
+
means = []
|
34
|
+
tries = 0
|
35
|
+
while means.size != count && tries < INIT_TRIES
|
36
|
+
(count - means.size).times { means << colors[rand(colors.size)] }
|
37
|
+
means.uniq!
|
38
|
+
tries += 1
|
39
|
+
end
|
40
|
+
|
41
|
+
means
|
42
|
+
end
|
43
|
+
|
44
|
+
def group_by_means(pixels, means)
|
45
|
+
groups = {}
|
46
|
+
|
47
|
+
pixels.each do |p|
|
48
|
+
distances = means.map { |m| p.rgb_distance(m) }
|
49
|
+
min_distance_index = distances.index(distances.min)
|
50
|
+
|
51
|
+
groups[min_distance_index] ||= []
|
52
|
+
groups[min_distance_index] << p
|
53
|
+
end
|
54
|
+
|
55
|
+
groups.values
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Camalian
|
4
|
+
module Quantization
|
5
|
+
class MedianCut # :nodoc:
|
6
|
+
SplitInfo = Struct.new(:range, :group_index, :color, keyword_init: true)
|
7
|
+
def process(colors, count)
|
8
|
+
# Its faster to extract unique colors once
|
9
|
+
colors = colors.uniq
|
10
|
+
groups = [colors]
|
11
|
+
limit = [count, colors.size].min
|
12
|
+
|
13
|
+
loop do
|
14
|
+
split_info = determine_group_split(groups)
|
15
|
+
group1, group2 = split_group(groups[split_info.group_index], split_info)
|
16
|
+
groups.reject!.each_with_index { |_g, index| index == split_info.group_index }
|
17
|
+
groups << group1 unless group1.empty?
|
18
|
+
groups << group2 unless group2.empty?
|
19
|
+
|
20
|
+
break if groups.size >= limit
|
21
|
+
end
|
22
|
+
|
23
|
+
palletes = groups.map { |g| Palette.new(g.uniq) }
|
24
|
+
average_colors = palletes.map(&:average_color)
|
25
|
+
|
26
|
+
Palette.new(average_colors[0...count])
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def determine_group_split(groups)
|
32
|
+
stats = []
|
33
|
+
|
34
|
+
groups.each_with_index do |group, index|
|
35
|
+
reds = group.map(&:r)
|
36
|
+
greens = group.map(&:g)
|
37
|
+
blues = group.map(&:b)
|
38
|
+
|
39
|
+
ranges = []
|
40
|
+
ranges << SplitInfo.new(group_index: index, range: reds.max - reds.min, color: :r) unless reds.empty?
|
41
|
+
ranges << SplitInfo.new(group_index: index, range: greens.max - greens.min, color: :g) unless greens.empty?
|
42
|
+
ranges << SplitInfo.new(group_index: index, range: blues.max - blues.min, color: :b) unless blues.empty?
|
43
|
+
|
44
|
+
stats << ranges.max_by(&:range)
|
45
|
+
end
|
46
|
+
|
47
|
+
stats.max_by(&:range)
|
48
|
+
end
|
49
|
+
|
50
|
+
def split_group(group, split_info)
|
51
|
+
split_color = split_info.color
|
52
|
+
colors = group.sort_by { |pixel| pixel.send split_color }
|
53
|
+
|
54
|
+
return [[colors[0]], [colors[1]]] if colors.size == 2
|
55
|
+
|
56
|
+
median_index = colors.size / 2
|
57
|
+
median_value = colors[median_index].send split_color
|
58
|
+
|
59
|
+
group1 = []
|
60
|
+
group2 = []
|
61
|
+
|
62
|
+
colors.each do |color|
|
63
|
+
if color.send(split_color) <= median_value
|
64
|
+
group1 << color
|
65
|
+
else
|
66
|
+
group2 << color
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
[group1, group2]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/camalian/version.rb
CHANGED
Binary file
|
data/test/color_test.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'minitest/spec'
|
6
|
+
require 'camalian'
|
7
|
+
|
8
|
+
describe Camalian::Color do
|
9
|
+
before do
|
10
|
+
@color = Camalian::Color.new(120, 255, 30)
|
11
|
+
end
|
12
|
+
|
13
|
+
describe 'Color initialized with 120, 255, 30 rgb values' do
|
14
|
+
it 'hex value must be #78ff1e' do
|
15
|
+
_(@color.to_hex).must_equal '#78ff1e'
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'hsl color components must ' do
|
19
|
+
_([@color.h.to_i, @color.s.to_i, @color.l.to_i]).must_equal [96, 100, 55]
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'hsv color components must ' do
|
23
|
+
_(@color.hsv.map(&:to_i)).must_equal [96, 88, 100]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe 'initialized with 1 integer rgb value' do
|
28
|
+
it 'must have leading zero' do
|
29
|
+
_(Camalian::Color.new(7, 7, 7).to_hex).must_equal '#070707'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'minitest/spec'
|
6
|
+
require 'camalian'
|
7
|
+
|
8
|
+
describe Camalian::Palette do
|
9
|
+
before do
|
10
|
+
mixed_colors = %w[#2560a2 #4dd915 #1530db #49cc23 #45c031 #3da84d #1d48be #399c5b #359069 #318478 #2d7886 #296c94
|
11
|
+
#2154b0 #193ccc #41b43f].freeze
|
12
|
+
|
13
|
+
@palette = Camalian::Palette.from_hex(mixed_colors)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'initialize palette with correct size' do
|
17
|
+
_(@palette.size).must_equal 15
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#sort_by_lightness' do
|
21
|
+
it 'sort colors by lightness ' do
|
22
|
+
_(@palette.sort_by_lightness.to_hex).must_equal ['#41b43f', '#45c031', '#1530db', '#49cc23', '#4dd915',
|
23
|
+
'#193ccc', '#3da84d', '#1d48be', '#399c5b', '#2154b0',
|
24
|
+
'#2560a2', '#359069', '#296c94', '#318478', '#2d7886']
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#sort_by_hue' do
|
29
|
+
it 'sort colors by hue' do
|
30
|
+
_(@palette.sort_by_hue.to_hex).must_equal ['#1530db', '#193ccc', '#1d48be', '#2154b0', '#2560a2', '#296c94',
|
31
|
+
'#2d7886', '#318478', '#359069', '#399c5b', '#3da84d', '#41b43f',
|
32
|
+
'#45c031', '#49cc23', '#4dd915']
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#sort_by_saturation' do
|
37
|
+
it 'sort colors by saturation' do
|
38
|
+
_(@palette.sort_by_saturation.to_hex).must_equal ['#1530db', '#4dd915', '#193ccc', '#1d48be', '#49cc23',
|
39
|
+
'#2154b0', '#2560a2', '#45c031', '#296c94', '#2d7886',
|
40
|
+
'#41b43f', '#3da84d', '#399c5b', '#359069', '#318478']
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '#sort_similar_colors' do
|
45
|
+
it 'sort colors by hsv' do
|
46
|
+
_(@palette.sort_similar_colors.to_hex).must_equal ['#4dd915', '#49cc23', '#45c031', '#41b43f', '#3da84d',
|
47
|
+
'#399c5b', '#359069', '#318478', '#2d7886', '#296c94',
|
48
|
+
'#2560a2', '#2154b0', '#1d48be', '#193ccc', '#1530db']
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '#average_color' do
|
53
|
+
it 'return one average color' do
|
54
|
+
_(@palette.average_color.to_hex).must_equal '#318477'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#light_colors' do
|
59
|
+
it 'extract light colors in provided limit' do
|
60
|
+
_(@palette.light_colors(40, 90).to_hex).must_equal ['#4dd915', '#1530db', '#49cc23', '#45c031', '#3da84d',
|
61
|
+
'#1d48be', '#399c5b', '#2154b0', '#193ccc', '#41b43f']
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'minitest/spec'
|
6
|
+
require 'camalian'
|
7
|
+
|
8
|
+
describe Camalian::Quantization::Histogram do
|
9
|
+
describe 'palette with 15 colors extracted' do
|
10
|
+
before do
|
11
|
+
@image = Camalian.load(File.join(File.dirname(__FILE__), '../assets/palette.png'))
|
12
|
+
@palette = @image.prominent_colors(15, optimal: false, quantization: Camalian::QUANTIZATION_HISTOGRAM)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'must have 15 colors' do
|
16
|
+
_(@palette.size).must_equal 15
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'sort similar colors in order' do
|
20
|
+
_(@palette.sort_similar_colors.map(&:to_hex)).must_equal PALLET_IMAGE_COLORS
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should extract distinct colors' do
|
25
|
+
colors = %w[#FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
|
26
|
+
result = Camalian::Quantization::Histogram.new.process(colors, 3)
|
27
|
+
|
28
|
+
_(result.size).must_equal 3
|
29
|
+
_(result).must_equal colors
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should extract distinct colors lesser than pixels' do
|
33
|
+
colors = %w[#FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
|
34
|
+
result = Camalian::Quantization::Histogram.new.process(colors, 2)
|
35
|
+
|
36
|
+
_(result.size).must_equal 2
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should extract distinct colors not more than pixels' do
|
40
|
+
colors = %w[#FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
|
41
|
+
result = Camalian::Quantization::Histogram.new.process(colors, 4)
|
42
|
+
|
43
|
+
_(result.size).must_equal 3
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should extract same color' do
|
47
|
+
colors = %w[#FF0000 #FF0000 #FF0000].map { |c| Camalian::Color.from_hex(c) }
|
48
|
+
result = Camalian::Quantization::Histogram.new.process(colors, 3)
|
49
|
+
|
50
|
+
_(result.size).must_equal 1
|
51
|
+
_(result.to_hex).must_equal ['#ff0000']
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should extract multiple colors' do
|
55
|
+
colors = %w[#FF0000 #FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
|
56
|
+
result = Camalian::Quantization::Histogram.new.process(colors, 3)
|
57
|
+
|
58
|
+
_(result.size).must_equal 3
|
59
|
+
_(result.to_hex).must_equal %w[#ff0000 #00ff00 #0000ff]
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'minitest/spec'
|
6
|
+
require 'camalian'
|
7
|
+
|
8
|
+
describe Camalian::Quantization::KMeans do
|
9
|
+
describe 'palette with 15 colors extracted' do
|
10
|
+
before do
|
11
|
+
@image = Camalian.load(File.join(File.dirname(__FILE__), '../assets/palette.png'))
|
12
|
+
@palette = @image.prominent_colors(15, optimal: false, quantization: Camalian::QUANTIZATION_K_MEANS)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'must have 15 colors' do
|
16
|
+
_(@palette.size).must_equal 15
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'sort similar colors in order' do
|
20
|
+
_(@palette.sort_similar_colors.map(&:to_hex)).must_equal PALLET_IMAGE_COLORS
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should extract distinct colors' do
|
25
|
+
colors = %w[#FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
|
26
|
+
result = Camalian::Quantization::KMeans.new.process(colors, 3)
|
27
|
+
|
28
|
+
_(result.size).must_equal 3
|
29
|
+
_(result).must_equal colors
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should extract distinct colors lesser than pixels' do
|
33
|
+
colors = %w[#FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
|
34
|
+
result = Camalian::Quantization::KMeans.new.process(colors, 2)
|
35
|
+
|
36
|
+
_(result.size).must_equal 2
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should extract distinct colors not more than pixels' do
|
40
|
+
colors = %w[#FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
|
41
|
+
result = Camalian::Quantization::KMeans.new.process(colors, 4)
|
42
|
+
|
43
|
+
_(result.size).must_equal 3
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should extract same color' do
|
47
|
+
colors = %w[#FF0000 #FF0000 #FF0000].map { |c| Camalian::Color.from_hex(c) }
|
48
|
+
result = Camalian::Quantization::KMeans.new.process(colors, 3)
|
49
|
+
|
50
|
+
_(result.size).must_equal 1
|
51
|
+
_(result.to_hex).must_equal ['#ff0000']
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should extract multiple colors' do
|
55
|
+
colors = %w[#FF0000 #FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
|
56
|
+
result = Camalian::Quantization::KMeans.new.process(colors, 3)
|
57
|
+
|
58
|
+
_(result.size).must_equal 3
|
59
|
+
_(result.to_hex).must_equal %w[#ff0000 #00ff00 #0000ff]
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'minitest/spec'
|
6
|
+
require 'camalian'
|
7
|
+
|
8
|
+
describe Camalian::Quantization::MedianCut do
|
9
|
+
describe 'palette with 15 colors extracted' do
|
10
|
+
before do
|
11
|
+
@image = Camalian.load(File.join(File.dirname(__FILE__), '../assets/palette.png'))
|
12
|
+
@palette = @image.prominent_colors(15, optimal: false, quantization: Camalian::QUANTIZATION_MEDIAN_CUT)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'must have 15 colors' do
|
16
|
+
_(@palette.size).must_equal 15
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'sort similar colors in order' do
|
20
|
+
_(@palette.sort_similar_colors.map(&:to_hex)).must_equal ['#4dd915', '#49cc23', '#45c031', '#41b43f', '#3da84d',
|
21
|
+
'#399c5b', '#359069', '#318478', '#2d7886', '#296c94',
|
22
|
+
'#2560a2', '#2154b0', '#1d48be', '#193ccc', '#1530db']
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should extract distinct colors' do
|
27
|
+
colors = %w[#FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
|
28
|
+
result = Camalian::Quantization::MedianCut.new.process(colors, 3)
|
29
|
+
|
30
|
+
_(result.size).must_equal 3
|
31
|
+
_(result.to_hex.sort).must_equal colors.map(&:to_hex).sort
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should extract distinct colors lesser than pixels' do
|
35
|
+
colors = %w[#FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
|
36
|
+
result = Camalian::Quantization::MedianCut.new.process(colors, 2)
|
37
|
+
|
38
|
+
_(result.size).must_equal 2
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should extract distinct colors not more than pixels' do
|
42
|
+
colors = %w[#FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
|
43
|
+
result = Camalian::Quantization::MedianCut.new.process(colors, 4)
|
44
|
+
|
45
|
+
_(result.size).must_equal 3
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should extract same color' do
|
49
|
+
colors = %w[#FF0000 #FF0000 #FF0000].map { |c| Camalian::Color.from_hex(c) }
|
50
|
+
result = Camalian::Quantization::MedianCut.new.process(colors, 3)
|
51
|
+
|
52
|
+
_(result.size).must_equal 1
|
53
|
+
_(result.to_hex).must_equal ['#ff0000']
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'only: should extract multiple colors' do
|
57
|
+
colors = %w[#FF0000 #FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
|
58
|
+
result = Camalian::Quantization::MedianCut.new.process(colors, 3)
|
59
|
+
|
60
|
+
_(result.size).must_equal 3
|
61
|
+
_(result.to_hex).must_equal ['#ff0000', '#0000ff', '#00ff00']
|
62
|
+
end
|
63
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'simplecov'
|
4
|
+
SimpleCov.start
|
5
|
+
|
6
|
+
require 'minitest/autorun'
|
7
|
+
require 'minitest/spec'
|
8
|
+
require 'camalian'
|
9
|
+
|
10
|
+
PALLET_IMAGE_COLORS = %w[#4dd915 #49cc23 #45c031 #41b43f #3da84d #399c5b #359069 #318478 #2d7886 #296c94 #2560a2
|
11
|
+
#2154b0 #1d48be #193ccc #1530db].freeze
|
metadata
CHANGED
@@ -1,16 +1,75 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: camalian
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
5
|
-
prerelease:
|
4
|
+
version: 0.2.0
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Nazar Hussain
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
13
|
-
dependencies:
|
11
|
+
date: 2021-04-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: chunky_png
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.3.14
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.3.14
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: minitest
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '5.14'
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 5.14.2
|
43
|
+
type: :development
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '5.14'
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 5.14.2
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: rake
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '13.0'
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 13.0.1
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '13.0'
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 13.0.1
|
14
73
|
description: Library used to deal with colors and images
|
15
74
|
email:
|
16
75
|
- nazarhussain@gmail.com
|
@@ -18,38 +77,63 @@ executables: []
|
|
18
77
|
extensions: []
|
19
78
|
extra_rdoc_files: []
|
20
79
|
files:
|
21
|
-
- .
|
80
|
+
- ".github/workflows/gpr-push.yml"
|
81
|
+
- ".github/workflows/ruby.yml"
|
82
|
+
- ".github/workflows/rubygems-push.yml"
|
83
|
+
- ".gitignore"
|
84
|
+
- ".overcommit.yml"
|
85
|
+
- ".rubocop.yml"
|
86
|
+
- ".tool-versions"
|
87
|
+
- ".travis.yml"
|
22
88
|
- Gemfile
|
23
89
|
- LICENSE.txt
|
24
90
|
- README.md
|
25
91
|
- Rakefile
|
26
92
|
- camalian.gemspec
|
27
93
|
- lib/camalian.rb
|
94
|
+
- lib/camalian/color.rb
|
95
|
+
- lib/camalian/image.rb
|
96
|
+
- lib/camalian/palette.rb
|
97
|
+
- lib/camalian/quantization/histogram.rb
|
98
|
+
- lib/camalian/quantization/k_means.rb
|
99
|
+
- lib/camalian/quantization/median_cut.rb
|
28
100
|
- lib/camalian/version.rb
|
29
|
-
|
101
|
+
- test/assets/palette.png
|
102
|
+
- test/color_test.rb
|
103
|
+
- test/palette_test.rb
|
104
|
+
- test/quantization/histogram_test.rb
|
105
|
+
- test/quantization/k_means_test.rb
|
106
|
+
- test/quantization/median_cut_test.rb
|
107
|
+
- test/test_helper.rb
|
108
|
+
homepage: https://github.com/nazarhussain/camalian
|
30
109
|
licenses:
|
31
110
|
- MIT
|
111
|
+
metadata: {}
|
32
112
|
post_install_message:
|
33
113
|
rdoc_options: []
|
34
114
|
require_paths:
|
35
115
|
- lib
|
36
116
|
required_ruby_version: !ruby/object:Gem::Requirement
|
37
|
-
none: false
|
38
117
|
requirements:
|
39
|
-
- -
|
118
|
+
- - ">="
|
40
119
|
- !ruby/object:Gem::Version
|
41
|
-
version: '
|
120
|
+
version: '2.4'
|
42
121
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
43
|
-
none: false
|
44
122
|
requirements:
|
45
|
-
- -
|
123
|
+
- - ">="
|
46
124
|
- !ruby/object:Gem::Version
|
47
125
|
version: '0'
|
48
126
|
requirements: []
|
49
|
-
|
50
|
-
rubygems_version: 1.8.23
|
127
|
+
rubygems_version: 3.0.3.1
|
51
128
|
signing_key:
|
52
|
-
specification_version:
|
129
|
+
specification_version: 4
|
53
130
|
summary: Library used to deal with colors and images. You can extract colors from
|
54
131
|
images.
|
55
|
-
test_files:
|
132
|
+
test_files:
|
133
|
+
- test/assets/palette.png
|
134
|
+
- test/color_test.rb
|
135
|
+
- test/palette_test.rb
|
136
|
+
- test/quantization/histogram_test.rb
|
137
|
+
- test/quantization/k_means_test.rb
|
138
|
+
- test/quantization/median_cut_test.rb
|
139
|
+
- test/test_helper.rb
|