falu 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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,11 @@
1
+ module Falu
2
+ module Colors
3
+ class Base
4
+ attr_reader :color
5
+
6
+ def as_json(options={})
7
+ to_s
8
+ end
9
+ end
10
+ end
11
+ 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
@@ -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