colordom 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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}")