falu 0.0.1
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.
- 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
|