colordom 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@ edition = "2018"
6
6
 
7
7
  # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8
8
  [dependencies]
9
+ magnus = "0.3"
9
10
  image = "0.24"
10
11
  dominant_color = "0.3"
11
12
  palette_extract = "0.1"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mkmf'
4
+ require 'rb_sys/mkmf'
5
+
6
+ create_rust_makefile('colordom/colordom')
@@ -0,0 +1,156 @@
1
+ use std::path::PathBuf;
2
+
3
+ use magnus::{
4
+ class, define_module, function, method,
5
+ prelude::*, gc::register_mark_object, memoize,
6
+ Error, ExceptionClass, RClass, RModule
7
+ };
8
+
9
+ use image::{DynamicImage};
10
+ use palette_extract::{Quality, MaxColors, PixelEncoding, PixelFilter};
11
+ use palette::{FromColor, IntoColor, Lab, Pixel, Srgb};
12
+
13
+ use dominant_color;
14
+ use kmeans_colors;
15
+
16
+ fn colordom_error() -> ExceptionClass {
17
+ *memoize!(ExceptionClass: {
18
+ let ex: RClass = class::object().const_get::<_, RModule>("Colordom").unwrap().const_get("Error").unwrap();
19
+ register_mark_object(ex);
20
+
21
+ ExceptionClass::from_value(*ex).unwrap()
22
+ })
23
+ }
24
+
25
+ #[derive(Clone)]
26
+ #[magnus::wrap(class = "Colordom::Color", free_immediatly, size)]
27
+ struct Color {
28
+ r: u8,
29
+ g: u8,
30
+ b: u8
31
+ }
32
+
33
+ impl Color {
34
+ fn new(r: u8, g: u8, b: u8) -> Self {
35
+ Self { r, g, b }
36
+ }
37
+
38
+ fn r(&self) -> u8 { self.r }
39
+ fn g(&self) -> u8 { self.g }
40
+ fn b(&self) -> u8 { self.b }
41
+
42
+ fn rgb(&self) -> Vec<u8> {
43
+ vec![self.r, self.g, self.b]
44
+ }
45
+
46
+ fn hex(&self) -> String {
47
+ format!("#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
48
+ }
49
+ }
50
+
51
+ #[derive(Clone)]
52
+ #[magnus::wrap(class = "Colordom::Image", free_immediatly, size)]
53
+ struct Image {
54
+ img: DynamicImage
55
+ }
56
+
57
+ impl Image {
58
+ fn new(path: PathBuf) -> Result<Self, Error> {
59
+ match image::open(&path) {
60
+ Ok(img) => Ok(Self { img }),
61
+ Err(ex) => Err(Error::new(colordom_error(), ex.to_string()))
62
+ }
63
+ }
64
+
65
+ fn pixels(&self) -> &[u8] {
66
+ self.img.as_bytes()
67
+ }
68
+
69
+ fn has_alpha(&self) -> bool {
70
+ self.img.color().has_alpha()
71
+ }
72
+
73
+ fn histogram(&self, max_colors: usize) -> Vec<Color> {
74
+ let colors = dominant_color::get_colors(
75
+ &self.pixels(),
76
+ self.has_alpha()
77
+ );
78
+
79
+ colors.chunks(3)
80
+ .take(max_colors)
81
+ .map(|x| Color::new(x[0], x[1], x[2]))
82
+ .collect::<Vec<Color>>()
83
+ }
84
+
85
+ fn mediancut(&self, max_colors: usize) -> Vec<Color> {
86
+ let colors = palette_extract::get_palette_with_options(
87
+ &self.pixels(),
88
+ PixelEncoding::Rgb,
89
+ Quality::new(6),
90
+ MaxColors::new(max_colors as u8),
91
+ PixelFilter::None
92
+ );
93
+
94
+ colors.iter()
95
+ .take(max_colors)
96
+ .map(|x| Color::new(x.r, x.g, x.b))
97
+ .collect::<Vec<Color>>()
98
+ }
99
+
100
+ fn kmeans(&self, max_colors: usize) -> Vec<Color> {
101
+ let max_iterations = 20;
102
+ let converge = 1.0;
103
+ let verbose = false;
104
+ let seed = 0;
105
+
106
+ let lab: Vec<Lab> = Srgb::from_raw_slice(&self.pixels()).iter()
107
+ .map(|x| x.into_format().into_color())
108
+ .collect();
109
+
110
+ let result = kmeans_colors::get_kmeans_hamerly(
111
+ max_colors, max_iterations, converge, verbose, &lab, seed
112
+ );
113
+
114
+ let colors = &result.centroids.iter()
115
+ .map(|x| Srgb::from_color(*x).into_format())
116
+ .collect::<Vec<Srgb<u8>>>();
117
+
118
+ colors.iter()
119
+ .take(max_colors)
120
+ .map(|x| Color::new(x.red, x.green, x.blue))
121
+ .collect::<Vec<Color>>()
122
+ }
123
+ }
124
+
125
+ #[magnus::init]
126
+ fn init() -> Result<(), Error> {
127
+ let module = define_module("Colordom")?;
128
+
129
+ let colorc = module.define_class("Color", Default::default())?;
130
+
131
+ colorc.define_singleton_method("new", function!(Color::new, 3))?;
132
+ colorc.define_method("r", method!(Color::r, 0))?;
133
+ colorc.define_method("g", method!(Color::g, 0))?;
134
+ colorc.define_method("b", method!(Color::b, 0))?;
135
+
136
+ colorc.define_method("rgb", method!(Color::rgb, 0))?;
137
+ colorc.define_method("hex", method!(Color::hex, 0))?;
138
+
139
+ colorc.define_method("clone", method!(Color::clone, 0))?;
140
+ colorc.define_method("dup", method!(Color::clone, 0))?;
141
+
142
+ colorc.define_alias("to_rgb", "rgb")?;
143
+ colorc.define_alias("to_hex", "hex")?;
144
+
145
+ let imagec = module.define_class("Image", Default::default())?;
146
+
147
+ imagec.define_singleton_method("new", function!(Image::new, 1))?;
148
+ imagec.define_method("histogram", method!(Image::histogram, 1))?;
149
+ imagec.define_method("mediancut", method!(Image::mediancut, 1))?;
150
+ imagec.define_method("kmeans", method!(Image::kmeans, 1))?;
151
+
152
+ imagec.define_method("clone", method!(Image::clone, 0))?;
153
+ imagec.define_method("dup", method!(Image::clone, 0))?;
154
+
155
+ Ok(())
156
+ }
@@ -1,25 +1,49 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Colordom
4
+ # Color object with RGB and HEX values implemented in Rust.
2
5
  class Color
3
- attr_reader :r, :g, :b
6
+ # @!method initialize(r, g, b)
7
+ # A new instance of Color.
8
+ # @return [Color]
4
9
 
5
- def initialize(red, green, blue)
6
- @r, @g, @b = [red, green, blue].map(&:to_i)
7
- end
10
+ # @!attribute [r] r
11
+ # Red color value.
12
+ # @return [Integer]
8
13
 
9
- def ==(other)
10
- other.is_a?(self.class) &&
11
- rgb == other.rgb
12
- end
14
+ # @!attribute [r] g
15
+ # Green color value.
16
+ # @return [Integer]
13
17
 
14
- def rgb
15
- [r, g, b]
16
- end
18
+ # @!attribute [r] b
19
+ # Blue color value.
20
+ # @return [Integer]
17
21
 
18
- def hex
19
- '#%02X%02X%02X' % rgb
20
- end
22
+ # @!method rgb
23
+ # @!parse [ruby] alias to_rgb rgb
24
+ # Get the RGB representation of the color.
25
+ # @return [Array<Integer>]
26
+
27
+ # @!method hex
28
+ # @!parse [ruby] alias to_hex hex
29
+ # Get the hex representation of the color.
30
+ # @return [String]
31
+
32
+ # Compare with other color value.
33
+ # @param other [Color, Array<Integer>, String]
34
+ # @return [Boolean]
21
35
 
22
- alias to_rgb rgb
23
- alias to_hex hex
36
+ def ==(other)
37
+ case other
38
+ when Array
39
+ rgb == other
40
+ when String
41
+ hex == other
42
+ when self.class
43
+ rgb == other.rgb
44
+ else
45
+ false
46
+ end
47
+ end
24
48
  end
25
49
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Colordom
2
4
  class Error < StandardError
3
5
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Colordom
4
+ # Image object implemented in Rust that extracts dominant colors.
5
+ class Image
6
+ # @!method initialize(path)
7
+ # A new instance of Image.
8
+ # @param path [String, Pathname, File]
9
+ # @return [Image]
10
+ # @raise [Error] if path is not a valid image
11
+
12
+ # @!method histogram(max_colors)
13
+ # Get dominant colors using histogram quantization.
14
+ # @param max_colors [Integer]
15
+ # @return [Array<Color>]
16
+
17
+ # @!method mediancut(max_colors)
18
+ # Get dominant colors using media cut quantization.
19
+ # @param max_colors [Integer]
20
+ # @return [Array<Color>]
21
+
22
+ # @!method kmeans(max_colors)
23
+ # Get dominant colors using k-means clustering.
24
+ # @param max_colors [Integer]
25
+ # @return [Array<Color>]
26
+ end
27
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Colordom
2
- VERSION = '0.2.0'.freeze
4
+ VERSION = '0.3.0'
3
5
  end
data/lib/colordom.rb CHANGED
@@ -1,56 +1,52 @@
1
- require 'ffi'
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ RUBY_VERSION =~ /(\d+\.\d+)/
5
+ require "colordom/#{$1}/colordom"
6
+ rescue LoadError
7
+ require 'colordom/colordom'
8
+ end
2
9
 
3
- require 'colordom/result'
4
- require 'colordom/native'
5
10
  require 'colordom/error'
6
11
  require 'colordom/color'
12
+ require 'colordom/image'
7
13
  require 'colordom/version'
8
14
 
15
+ # Module that extracts dominant colors from images
16
+ # using native extension implemented in Rust.
9
17
  module Colordom
10
18
  class << self
11
- def histogram(path, max_colors = 5)
12
- regex = /(\d+), (\d+), (\d+)/
13
- value = call(:to_histogram, path)
14
-
15
- parse(value, regex, max_colors)
16
- end
17
-
18
- def mediancut(path, max_colors = 5)
19
- regex = /r: (\d+), g: (\d+), b: (\d+)/
20
- value = call(:to_mediancut, path, max_colors)
21
-
22
- parse(value, regex, max_colors)
23
- end
19
+ # Get dominant colors using histogram quantization.
20
+ # @param path (see Image#initialize)
21
+ # @param max_colors (see Image#histogram)
22
+ # @return (see Image#histogram)
23
+ # @raise (see Image#initialize)
24
24
 
25
- def kmeans(path, max_colors = 5)
26
- regex = /red: (\d+), green: (\d+), blue: (\d+)/
27
- value = call(:to_kmeans, path, max_colors)
28
-
29
- parse(value, regex)
25
+ def histogram(path, max_colors = 5)
26
+ image = Image.new(path)
27
+ image.histogram(max_colors)
30
28
  end
31
29
 
32
- private
33
-
34
- def call(method, path, *args)
35
- return if path.nil?
36
-
37
- result = Native.send(method, path, *args)
38
- result = result.read_string.force_encoding('UTF-8')
30
+ # Get dominant colors using media cut quantization.
31
+ # @param path (see Image#initialize)
32
+ # @param max_colors (see Image#mediancut)
33
+ # @return (see Image#mediancut)
34
+ # @raise (see Image#initialize)
39
35
 
40
- raise Error, result if result.start_with?(Error.name)
41
-
42
- result
36
+ def mediancut(path, max_colors = 5)
37
+ image = Image.new(path)
38
+ image.mediancut(max_colors)
43
39
  end
44
40
 
45
- def parse(result, regex, limit = nil)
46
- return [] if result.nil?
41
+ # Get dominant colors using k-means clustering.
42
+ # @param path (see Image#initialize)
43
+ # @param max_colors (see Image#kmeans)
44
+ # @return (see Image#kmeans)
45
+ # @raise (see Image#initialize)
47
46
 
48
- colors = result.scan(regex)
49
- colors = colors.first(limit) if limit && limit.positive?
50
-
51
- colors.map do |values|
52
- Color.new(*values)
53
- end
47
+ def kmeans(path, max_colors = 5)
48
+ image = Image.new(path)
49
+ image.kmeans(max_colors)
54
50
  end
55
51
  end
56
52
  end
metadata CHANGED
@@ -1,31 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: colordom
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonian Guveli
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-20 00:00:00.000000000 Z
11
+ date: 2022-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: ffi
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: thermite
14
+ name: rb_sys
29
15
  requirement: !ruby/object:Gem::Requirement
30
16
  requirements:
31
17
  - - ">="
@@ -67,48 +53,39 @@ dependencies:
67
53
  - !ruby/object:Gem::Version
68
54
  version: '5.0'
69
55
  - !ruby/object:Gem::Dependency
70
- name: rake
56
+ name: rake-compiler
71
57
  requirement: !ruby/object:Gem::Requirement
72
58
  requirements:
73
59
  - - "~>"
74
60
  - !ruby/object:Gem::Version
75
- version: '13.0'
61
+ version: '1.2'
76
62
  type: :development
77
63
  prerelease: false
78
64
  version_requirements: !ruby/object:Gem::Requirement
79
65
  requirements:
80
66
  - - "~>"
81
67
  - !ruby/object:Gem::Version
82
- version: '13.0'
68
+ version: '1.2'
83
69
  description: Extract dominant colors from images using native extension implemented
84
70
  in Rust.
85
71
  email:
86
72
  - jonian@hardpixel.eu
87
73
  executables: []
88
74
  extensions:
89
- - ext/Rakefile
75
+ - ext/colordom/extconf.rb
90
76
  extra_rdoc_files: []
91
77
  files:
92
- - ".gitignore"
93
- - ".travis.yml"
94
- - Cargo.toml
95
- - Gemfile
96
78
  - LICENSE.txt
97
79
  - README.md
98
- - Rakefile
99
- - bin/benchmark
100
- - bin/compare
101
- - bin/console
102
- - bin/setup
103
- - colordom.gemspec
104
- - ext/Rakefile
80
+ - ext/colordom/Cargo.lock
81
+ - ext/colordom/Cargo.toml
82
+ - ext/colordom/extconf.rb
83
+ - ext/colordom/src/lib.rs
105
84
  - lib/colordom.rb
106
85
  - lib/colordom/color.rb
107
86
  - lib/colordom/error.rb
108
- - lib/colordom/native.rb
109
- - lib/colordom/result.rb
87
+ - lib/colordom/image.rb
110
88
  - lib/colordom/version.rb
111
- - src/lib.rs
112
89
  homepage: https://github.com/hardpixel/colordom
113
90
  licenses:
114
91
  - MIT
@@ -121,14 +98,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
121
98
  requirements:
122
99
  - - ">="
123
100
  - !ruby/object:Gem::Version
124
- version: '0'
101
+ version: '2.6'
125
102
  required_rubygems_version: !ruby/object:Gem::Requirement
126
103
  requirements:
127
104
  - - ">="
128
105
  - !ruby/object:Gem::Version
129
106
  version: '0'
130
107
  requirements: []
131
- rubygems_version: 3.1.6
108
+ rubygems_version: 3.3.7
132
109
  signing_key:
133
110
  specification_version: 4
134
111
  summary: Extract dominant colors from images
data/.gitignore DELETED
@@ -1,17 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /doc/
6
- /pkg/
7
- /spec/reports/
8
- /tmp/
9
- Gemfile.lock
10
-
11
- # Added by cargo
12
-
13
- /target
14
- Cargo.lock
15
-
16
- /lib/*.so
17
- /mkmf.log
data/.travis.yml DELETED
@@ -1,13 +0,0 @@
1
- ---
2
- language: ruby
3
- cache: bundler
4
- rvm:
5
- - 2.7.2
6
- env:
7
- global:
8
- - RUST_VERSION=stable
9
- before_install:
10
- - if [ ! -e "$HOME/.cargo/bin" ]; then curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain $RUST_VERSION -y; fi
11
- - export PATH="$HOME/.cargo/bin:$PATH"
12
- - rustup default $RUST_VERSION
13
- - gem install bundler -v 2.1.4
data/Gemfile DELETED
@@ -1,11 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in colordom.gemspec
4
- gemspec
5
-
6
- gem 'benchmark-ips'
7
- gem 'benchmark-memory'
8
- gem 'camalian'
9
- gem 'colorscore'
10
- gem 'miro', github: 'jonbuda/miro'
11
- gem 'chunky_png', '1.3.15'
data/Rakefile DELETED
@@ -1,13 +0,0 @@
1
- require 'bundler/gem_tasks'
2
- require 'rake/testtask'
3
- require 'thermite/tasks'
4
-
5
- Thermite::Tasks.new
6
-
7
- Rake::TestTask.new(:test) do |t|
8
- t.libs << 'test'
9
- t.libs << 'lib'
10
- t.test_files = FileList['test/**/*_test.rb']
11
- end
12
-
13
- task default: %w[thermite:build test]
data/bin/benchmark DELETED
@@ -1,80 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'bundler/setup'
4
-
5
- require 'benchmark/ips'
6
- require 'benchmark/memory'
7
-
8
- require 'colordom'
9
- require 'camalian'
10
- require 'colorscore'
11
- require 'miro'
12
-
13
- COUNT = 5
14
- IMAGE = File.join(__dir__, '..', 'samples/sample.png')
15
-
16
- def miro(method)
17
- Miro.options[:color_count] = COUNT
18
- Miro.options[:method] = method
19
-
20
- colors = Miro::DominantColors.new(IMAGE)
21
- colors.to_hex
22
- end
23
-
24
- def colorscore
25
- histogram = Colorscore::Histogram.new(IMAGE, COUNT)
26
- histogram.colors
27
- end
28
-
29
- def camalian(quant)
30
- img = Camalian::load(IMAGE)
31
- img.prominent_colors(COUNT, quantization: quant)
32
- end
33
-
34
- reports = lambda do |x|
35
- x.report('colordom (HST)') do
36
- Colordom.histogram(IMAGE, COUNT)
37
- end
38
-
39
- x.report('colordom (MCQ)') do
40
- Colordom.mediancut(IMAGE, COUNT)
41
- end
42
-
43
- x.report('colordom (KMS)') do
44
- Colordom.kmeans(IMAGE, COUNT)
45
- end
46
-
47
- x.report('colorscore (HST)') do
48
- colorscore
49
- end
50
-
51
- x.report('miro (HST)') do
52
- miro('histogram')
53
- end
54
-
55
- x.report('miro (PXG)') do
56
- miro('pixel_group')
57
- end
58
-
59
- x.report('camalian (HST)') do
60
- camalian(Camalian::QUANTIZATION_HISTOGRAM)
61
- end
62
-
63
- x.report('camalian (MCQ)') do
64
- camalian(Camalian::QUANTIZATION_MEDIAN_CUT)
65
- end
66
-
67
- x.report('camalian (KMS)') do
68
- camalian(Camalian::QUANTIZATION_K_MEANS)
69
- end
70
- end
71
-
72
- Benchmark.ips do |x|
73
- reports.call(x)
74
- x.compare!
75
- end
76
-
77
- Benchmark.memory do |x|
78
- reports.call(x)
79
- x.compare!
80
- end
data/bin/compare DELETED
@@ -1,68 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'bundler/setup'
4
-
5
- require 'tempfile'
6
- require 'base64'
7
-
8
- require 'colordom'
9
- require 'camalian'
10
- require 'colorscore'
11
- require 'miro'
12
-
13
- COUNT = 6
14
- IMAGE = File.join(__dir__, '..', 'samples/compare.png')
15
- TEMPL = File.read File.join(__dir__, '..', 'samples/compare.svg')
16
-
17
- TEMPL.sub!('compare.png', "data:image/png;base64,#{Base64.strict_encode64(File.read(IMAGE))}")
18
-
19
- def replace(prefix, colors)
20
- colors.each_with_index do |color, index|
21
- TEMPL.sub!("{{#{prefix}-#{index}}}", color)
22
- end
23
- end
24
-
25
- def miro(method, prefix)
26
- Miro.options[:color_count] = COUNT
27
- Miro.options[:method] = method
28
-
29
- colors = Miro::DominantColors.new(IMAGE)
30
- replace("miro-#{prefix}", colors.to_hex)
31
- end
32
-
33
- def colorscore(prefix)
34
- histogram = Colorscore::Histogram.new(IMAGE, COUNT)
35
- replace("colorscore-#{prefix}", histogram.colors.map(&:html))
36
- end
37
-
38
- def camalian(quant, prefix)
39
- image = Camalian::load(IMAGE)
40
- colors = image.prominent_colors(COUNT, quantization: quant)
41
-
42
- replace("camalian-#{prefix}", colors.map(&:to_hex))
43
- end
44
-
45
- def colordom(method, prefix)
46
- colors = Colordom.send(method, IMAGE, COUNT)
47
- replace("colordom-#{prefix}", colors.map(&:to_hex))
48
- end
49
-
50
- colordom(:histogram, 'hst')
51
- colordom(:mediancut, 'mcq')
52
- colordom(:kmeans, 'kms')
53
-
54
- camalian(Camalian::QUANTIZATION_HISTOGRAM, 'hst')
55
- camalian(Camalian::QUANTIZATION_MEDIAN_CUT, 'mcq')
56
- camalian(Camalian::QUANTIZATION_K_MEANS, 'kms')
57
-
58
- colorscore('hst')
59
-
60
- miro('histogram', 'hst')
61
- miro('pixel_group', 'pxg')
62
-
63
- tempfile = Tempfile.create(['colordom-compare', '.svg'])
64
-
65
- tempfile.write(TEMPL)
66
- tempfile.close
67
-
68
- system("xdg-open #{tempfile.path}")