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.
@@ -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