falu 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/colors.txt +746 -0
- data/lib/falu.rb +20 -0
- data/lib/falu/color.rb +87 -0
- data/lib/falu/colors/base.rb +11 -0
- data/lib/falu/colors/hex.rb +56 -0
- data/lib/falu/colors/hsl.rb +73 -0
- data/lib/falu/colors/rgb.rb +101 -0
- data/lib/falu/dithered_palette.rb +15 -0
- data/lib/falu/image.rb +121 -0
- data/lib/falu/image_palette.rb +21 -0
- data/lib/falu/palette.rb +145 -0
- data/lib/falu/swatch.rb +64 -0
- data/lib/falu/version.rb +14 -0
- data/spec/spec_helper.rb +71 -0
- metadata +129 -0
data/lib/falu.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'canfig'
|
2
|
+
require 'active_support/core_ext/module'
|
3
|
+
require 'active_support/core_ext/object/json'
|
4
|
+
require 'mini_magick'
|
5
|
+
require 'falu/image'
|
6
|
+
require 'falu/color'
|
7
|
+
require 'falu/swatch'
|
8
|
+
require 'falu/palette'
|
9
|
+
require 'falu/image_palette'
|
10
|
+
require 'falu/dithered_palette'
|
11
|
+
|
12
|
+
module Falu
|
13
|
+
|
14
|
+
def self.colors
|
15
|
+
@colors ||= File.read(File.expand_path('../../colors.txt', __FILE__)).split("\n").map do |c|
|
16
|
+
carr = c.split("|")
|
17
|
+
{name: carr[0], hex: carr[1], rgb: carr[2], hsl: carr[3]}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/falu/color.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'falu/colors/hex'
|
2
|
+
require 'falu/colors/rgb'
|
3
|
+
require 'falu/colors/hsl'
|
4
|
+
|
5
|
+
module Falu
|
6
|
+
class Color
|
7
|
+
attr_reader :color
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def dump(color)
|
11
|
+
color.as_json
|
12
|
+
end
|
13
|
+
|
14
|
+
def load(json)
|
15
|
+
new(json.values.first)
|
16
|
+
end
|
17
|
+
|
18
|
+
def color(color)
|
19
|
+
if hex?(color)
|
20
|
+
Falu::Colors::HEX.new(color)
|
21
|
+
elsif rgb?(color) && !hsl?(color)
|
22
|
+
Falu::Colors::RGB.new(color)
|
23
|
+
elsif hsl?(color) && !rgb?(color)
|
24
|
+
Falu::Colors::HSL.new(color)
|
25
|
+
else
|
26
|
+
raise "Unable to determine color type (RGB vs HSL): #{color.to_s}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def hex?(color)
|
31
|
+
color = color.join('') if color.is_a?(Array)
|
32
|
+
!!color.to_s.match(/#?([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})/)
|
33
|
+
end
|
34
|
+
|
35
|
+
def rgb?(color)
|
36
|
+
color = color.to_s.split(',') unless color.is_a?(Array)
|
37
|
+
color = color.map { |c| c.to_i }
|
38
|
+
return false if color.length != 3
|
39
|
+
return false if color.any? { |c| c > 255 || c < 0 }
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
def hsl?(color)
|
44
|
+
color = color.to_s.split(',') unless color.is_a?(Array)
|
45
|
+
color = color.map { |c| c.to_f }
|
46
|
+
return false if color.length != 3
|
47
|
+
return false if color[0] > 360 || color[0] < 0
|
48
|
+
return false if color[1..2].any? { |c| c > 100 || c < 0 }
|
49
|
+
true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def initialize(color)
|
54
|
+
@color = color.is_a?(Falu::Colors::Base) ? color : self.class.color(color)
|
55
|
+
end
|
56
|
+
|
57
|
+
def name
|
58
|
+
cname = Falu.colors.find { |c| c[:hex].downcase == hex.to_s.downcase }
|
59
|
+
cname.nil? ? cname : cname[:name]
|
60
|
+
end
|
61
|
+
|
62
|
+
def hex
|
63
|
+
@hex ||= color.to_hex
|
64
|
+
end
|
65
|
+
|
66
|
+
def rgb
|
67
|
+
@rgb ||= color.to_rgb
|
68
|
+
end
|
69
|
+
|
70
|
+
def hsl
|
71
|
+
@hsl ||= color.to_hsl
|
72
|
+
end
|
73
|
+
|
74
|
+
def <=>(other)
|
75
|
+
rgb.absolute <=> other.rgb.absolute
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_s(stype=:hex)
|
79
|
+
send(stype).to_s
|
80
|
+
end
|
81
|
+
|
82
|
+
def as_json(options={})
|
83
|
+
# TODO options to control what type to return
|
84
|
+
hex.as_json
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'falu/colors/base'
|
2
|
+
|
3
|
+
module Falu
|
4
|
+
module Colors
|
5
|
+
class HEX < Base
|
6
|
+
def initialize(*color, rgb: nil, hsl: nil)
|
7
|
+
color = color[0] if color.length == 1
|
8
|
+
raise "Invalid HEX Color: #{color.to_s}" unless Falu::Color.hex?(color)
|
9
|
+
@color = color.is_a?(Array) ? color.join : color
|
10
|
+
end
|
11
|
+
|
12
|
+
def colors
|
13
|
+
@colors ||= begin
|
14
|
+
match = color.to_s.match(/#?([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})/)
|
15
|
+
match[1..3] unless match.nil?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def red
|
20
|
+
colors[0]
|
21
|
+
end
|
22
|
+
|
23
|
+
def green
|
24
|
+
colors[1]
|
25
|
+
end
|
26
|
+
|
27
|
+
def blue
|
28
|
+
colors[2]
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
"##{colors.join}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_a
|
36
|
+
colors
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_h
|
40
|
+
{red: red, green: green, blue: blue}
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_hex
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_rgb
|
48
|
+
Falu::Colors::RGB.new(colors.map { |c| c.to_i(16) })
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_hsl
|
52
|
+
to_rgb.to_hsl
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'falu/colors/base'
|
2
|
+
|
3
|
+
module Falu
|
4
|
+
module Colors
|
5
|
+
class HSL < Base
|
6
|
+
def initialize(*color, hex: nil, rgb: nil)
|
7
|
+
color = color[0] if color.length == 1
|
8
|
+
raise "Invalid HSL Color: #{color.to_s}" unless Falu::Color.hsl?(color)
|
9
|
+
@color = color.is_a?(Array) ? color.join(',') : color
|
10
|
+
end
|
11
|
+
|
12
|
+
def colors
|
13
|
+
@colors ||= color.to_s.split(',').map { |c| c.strip.to_f }
|
14
|
+
end
|
15
|
+
|
16
|
+
def hue
|
17
|
+
colors[0]
|
18
|
+
end
|
19
|
+
|
20
|
+
def saturation
|
21
|
+
colors[1]
|
22
|
+
end
|
23
|
+
|
24
|
+
def lightness
|
25
|
+
colors[2]
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
colors.map { |c| c.round.to_s.rjust(3, '0') }.join(',')
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_a
|
33
|
+
colors
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_h
|
37
|
+
{hue: hue, saturation: saturation, lightness: lightness}
|
38
|
+
end
|
39
|
+
|
40
|
+
def hue_to_rgb(p, q, t)
|
41
|
+
t += 1.0 if t < 0
|
42
|
+
t -= 1.0 if t > 1
|
43
|
+
return p + (q - p) * 6.0 * t if t < (1.0/6.0)
|
44
|
+
return q if t < (1.0/2.0)
|
45
|
+
return p + (q - p) * (2.0/3.0 - t) * 6.0 if t < (2.0/3.0)
|
46
|
+
return p
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_hsl
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_rgb
|
54
|
+
red = grn = blu = lit = (lightness.to_f / 100.0)
|
55
|
+
hue, sat = (self.hue.to_f / 360.0), (saturation.to_f / 100.0)
|
56
|
+
|
57
|
+
if sat > 0
|
58
|
+
lum = lit < 0.5 ? lit * (1.0 + sat) : (lit + sat) - (lit * sat)
|
59
|
+
crm = (2.0 * lit) - lum
|
60
|
+
red = hue_to_rgb(crm, lum, hue + 1.0/3.0)
|
61
|
+
grn = hue_to_rgb(crm, lum, hue)
|
62
|
+
blu = hue_to_rgb(crm, lum, hue - 1.0/3.0)
|
63
|
+
end
|
64
|
+
|
65
|
+
Falu::Colors::RGB.new([(red * 255).round, (grn * 255).round, (blu * 255).round])
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_hex
|
69
|
+
to_rgb.to_hex
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'falu/colors/base'
|
2
|
+
|
3
|
+
module Falu
|
4
|
+
module Colors
|
5
|
+
class RGB < Base
|
6
|
+
def initialize(*color)
|
7
|
+
color = color[0] if color.length == 1
|
8
|
+
raise "Invalid RGB Color: #{color.to_s}" unless Falu::Color.rgb?(color)
|
9
|
+
@color = color.is_a?(Array) ? color.join(',') : color
|
10
|
+
end
|
11
|
+
|
12
|
+
def colors
|
13
|
+
@colors ||= color.to_s.split(',').map { |c| c.strip.to_i }
|
14
|
+
end
|
15
|
+
|
16
|
+
def red
|
17
|
+
colors[0]
|
18
|
+
end
|
19
|
+
|
20
|
+
def green
|
21
|
+
colors[1]
|
22
|
+
end
|
23
|
+
|
24
|
+
def blue
|
25
|
+
colors[2]
|
26
|
+
end
|
27
|
+
|
28
|
+
def absolute
|
29
|
+
colors.sum
|
30
|
+
end
|
31
|
+
|
32
|
+
def inverse
|
33
|
+
@inverse ||= colors.map { |c| c / 255.0 }
|
34
|
+
end
|
35
|
+
|
36
|
+
def chroma
|
37
|
+
@chroma ||= (inverse.max - inverse.min)
|
38
|
+
end
|
39
|
+
|
40
|
+
def lightness
|
41
|
+
@lightness ||= ((inverse.max + inverse.min) / 2)
|
42
|
+
end
|
43
|
+
|
44
|
+
def saturation
|
45
|
+
@saturation ||= begin
|
46
|
+
if inverse.max == inverse.min
|
47
|
+
0
|
48
|
+
else
|
49
|
+
if lightness < 0.5
|
50
|
+
(chroma / (inverse.max + inverse.min))
|
51
|
+
else
|
52
|
+
(chroma / (2 - inverse.max - inverse.min))
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def hue
|
59
|
+
@hue ||= begin
|
60
|
+
if inverse.max == inverse.min
|
61
|
+
0
|
62
|
+
else
|
63
|
+
rd,gr,bl = *inverse
|
64
|
+
|
65
|
+
if rd == inverse.max
|
66
|
+
hue = ((gr - bl) / chroma % 6) * 60
|
67
|
+
elsif gr == inverse.max
|
68
|
+
hue = ((bl - rd) / chroma + 2) * 60
|
69
|
+
elsif bl == inverse.max
|
70
|
+
hue = ((rd - gr) / chroma + 4) * 60
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_s
|
77
|
+
colors.map { |c| c.round.to_s.rjust(3, '0') }.join(',')
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_a
|
81
|
+
colors
|
82
|
+
end
|
83
|
+
|
84
|
+
def to_h
|
85
|
+
{red: red, green: green, blue: blue}
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_rgb
|
89
|
+
self
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_hex
|
93
|
+
Falu::Colors::HEX.new("##{colors.map { |c| c.to_s(16).rjust(2, '0') }.join}")
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_hsl
|
97
|
+
Falu::Colors::HSL.new([hue, (saturation * 100), (lightness * 100)])
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Falu
|
2
|
+
class DitheredPalette < ImagePalette
|
3
|
+
delegate :colors, to: :configuration
|
4
|
+
|
5
|
+
def initialize(image, swatches=nil, **opts)
|
6
|
+
configuration.configure({colors: 20})
|
7
|
+
super(image, swatches, **opts)
|
8
|
+
end
|
9
|
+
|
10
|
+
def swatches
|
11
|
+
@swatches = image.scale(scale).dither(colors).sample(0, 0, size: size, sample: sample).map { |swatch| Falu::Swatch.new(*swatch) } if @swatches.empty?
|
12
|
+
@swatches
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/falu/image.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
# TODO currently this relies on shelling out to imagemagick via minimagick, but could be improved
|
4
|
+
#
|
5
|
+
# Image
|
6
|
+
# Images::
|
7
|
+
# Image
|
8
|
+
# Original
|
9
|
+
# Dithered
|
10
|
+
# Scaled
|
11
|
+
|
12
|
+
module Falu
|
13
|
+
class Image
|
14
|
+
|
15
|
+
delegate :width, :height, :path, :run_command, to: :image
|
16
|
+
|
17
|
+
def initialize(image)
|
18
|
+
@image = image
|
19
|
+
end
|
20
|
+
|
21
|
+
def image
|
22
|
+
@image = MiniMagick::Image.open(@image) unless @image.is_a?(MiniMagick::Image)
|
23
|
+
@image
|
24
|
+
end
|
25
|
+
|
26
|
+
def histogram
|
27
|
+
#hist = run_command("convert", "#{path}", "-format", "%c", "histogram:info:").split("\n")
|
28
|
+
#return hist.first
|
29
|
+
run_command("convert", "#{path}", "-format", "%c", "histogram:info:").split("\n").map do |clr|
|
30
|
+
next unless match = clr.match(/\s*(\d+):\s+\((\d+,\d+,\d+)\)\s+(#[\da-fA-F]{6})\s+(.*)/)
|
31
|
+
color = {
|
32
|
+
count: match[1].to_i,
|
33
|
+
rgb: match[2],
|
34
|
+
hex: match[3].downcase
|
35
|
+
}
|
36
|
+
|
37
|
+
if block_given?
|
38
|
+
yield(*(color.values + [color]))
|
39
|
+
else
|
40
|
+
color
|
41
|
+
end
|
42
|
+
end.compact.to_enum
|
43
|
+
end
|
44
|
+
|
45
|
+
def pixels(x=0, y=0, w=nil, h=nil, size: 10, sample: nil, &block)
|
46
|
+
w ||= width
|
47
|
+
h ||= height
|
48
|
+
|
49
|
+
(w / size).times.map do |ww|
|
50
|
+
pw = (ww * size)
|
51
|
+
px = x + pw
|
52
|
+
|
53
|
+
(h / size).times.map do |hh|
|
54
|
+
ph = (hh * size)
|
55
|
+
py = y + ph
|
56
|
+
|
57
|
+
pixels = run_command("convert", "#{path}[#{size}x#{size}+#{px}+#{py}]", "-depth", '8', "txt:").split("\n")
|
58
|
+
sample = 1 if sample == true
|
59
|
+
pixels = pixels.sample(sample) if sample
|
60
|
+
|
61
|
+
pixels.map do |pxl|
|
62
|
+
next unless match = pxl.match(/(\d+),(\d+):\s+\((\d+,\d+,\d+)\)\s+(#[\da-fA-F]{6})\s+(.*)/)
|
63
|
+
|
64
|
+
pixel = {
|
65
|
+
x: match[1].to_i + pw,
|
66
|
+
y: match[2].to_i + ph,
|
67
|
+
rgb: match[3],
|
68
|
+
hex: match[4].downcase,
|
69
|
+
}
|
70
|
+
|
71
|
+
if block_given?
|
72
|
+
yield(*(pixel.values + [pixel]))
|
73
|
+
else
|
74
|
+
pixel
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end.flatten.to_enum
|
79
|
+
end
|
80
|
+
|
81
|
+
def sample(x=0, y=0, w=nil, h=nil, size: 10, sample: false, &block)
|
82
|
+
palette = {}
|
83
|
+
pixels(x, y, w, h, size: size, sample: sample) do |x,y,color|
|
84
|
+
palette[color] ||= 0
|
85
|
+
palette[color] += 1
|
86
|
+
yield(color) if block_given?
|
87
|
+
end
|
88
|
+
palette.to_a.to_enum
|
89
|
+
end
|
90
|
+
|
91
|
+
def scale(scale, filename: nil)
|
92
|
+
filename ||= Tempfile.new(['scaled', '.png']).path
|
93
|
+
|
94
|
+
args = [
|
95
|
+
'convert', path,
|
96
|
+
'-scale', scale,
|
97
|
+
filename
|
98
|
+
]
|
99
|
+
|
100
|
+
run_command(*args)
|
101
|
+
self.class.new(filename)
|
102
|
+
end
|
103
|
+
|
104
|
+
def dither(colors=3, filename: nil, scale: nil, unique: false)
|
105
|
+
filename ||= Tempfile.new(['dithered', '.png']).path
|
106
|
+
scale ||= unique ? (colors * 10) : [width, height].max
|
107
|
+
|
108
|
+
args = [
|
109
|
+
'convert', path,
|
110
|
+
'+dither',
|
111
|
+
'-colors', colors,
|
112
|
+
(unique ? '-unique-colors' : nil),
|
113
|
+
'-scale', scale,
|
114
|
+
filename
|
115
|
+
].compact
|
116
|
+
|
117
|
+
run_command(*args)
|
118
|
+
self.class.new(filename)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|