image_util 0.1.0 → 0.3.0
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 +4 -4
- data/AGENTS.md +5 -6
- data/CHANGELOG.md +41 -6
- data/README.md +229 -81
- data/Rakefile +5 -0
- data/docs/cli.md +5 -0
- data/docs/samples/background.png +0 -0
- data/docs/samples/bitmap_text.png +0 -0
- data/docs/samples/colors.png +0 -0
- data/docs/samples/constructor.png +0 -0
- data/docs/samples/dither.png +0 -0
- data/docs/samples/draw.png +0 -0
- data/docs/samples/iterator.png +0 -0
- data/docs/samples/paste.png +0 -0
- data/docs/samples/pdither.png +0 -0
- data/docs/samples/pipe.png +0 -0
- data/docs/samples/range.png +0 -0
- data/docs/samples/redimension.png +0 -0
- data/docs/samples/resize.png +0 -0
- data/docs/samples/sixel.png +0 -0
- data/docs/samples/transform.png +0 -0
- data/exe/image_util +7 -0
- data/lib/image_util/benchmarking.rb +25 -0
- data/lib/image_util/bitmap_font/fonts/smfont/charset.txt +1 -0
- data/lib/image_util/bitmap_font/fonts/smfont/font.png +0 -0
- data/lib/image_util/bitmap_font.rb +72 -0
- data/lib/image_util/cli.rb +54 -0
- data/lib/image_util/codec/chunky_png.rb +67 -0
- data/lib/image_util/codec/image_magick.rb +82 -15
- data/lib/image_util/codec/kitty.rb +81 -0
- data/lib/image_util/codec/libpng.rb +2 -10
- data/lib/image_util/codec/libsixel.rb +14 -14
- data/lib/image_util/codec/libturbojpeg.rb +1 -11
- data/lib/image_util/codec/pam.rb +24 -22
- data/lib/image_util/codec/ruby_sixel.rb +12 -13
- data/lib/image_util/codec.rb +5 -1
- data/lib/image_util/color/css_colors.rb +158 -0
- data/lib/image_util/color.rb +67 -14
- data/lib/image_util/extension.rb +24 -0
- data/lib/image_util/filter/_mixin.rb +9 -0
- data/lib/image_util/filter/background.rb +4 -4
- data/lib/image_util/filter/bitmap_text.rb +17 -0
- data/lib/image_util/filter/colors.rb +21 -0
- data/lib/image_util/filter/draw.rb +22 -9
- data/lib/image_util/filter/palette.rb +197 -0
- data/lib/image_util/filter/paste.rb +1 -1
- data/lib/image_util/filter/redimension.rb +83 -0
- data/lib/image_util/filter/resize.rb +1 -1
- data/lib/image_util/filter/transform.rb +48 -0
- data/lib/image_util/filter.rb +5 -1
- data/lib/image_util/generator/bitmap_text.rb +38 -0
- data/lib/image_util/generator/example/rose.png +0 -0
- data/lib/image_util/generator/example.rb +9 -0
- data/lib/image_util/generator.rb +8 -0
- data/lib/image_util/image/buffer.rb +11 -11
- data/lib/image_util/image.rb +54 -26
- data/lib/image_util/magic.rb +8 -6
- data/lib/image_util/statistic/{color.rb → colors.rb} +2 -2
- data/lib/image_util/statistic.rb +1 -1
- data/lib/image_util/terminal.rb +61 -0
- data/lib/image_util/version.rb +1 -1
- data/lib/image_util/view/interpolated.rb +1 -1
- data/lib/image_util.rb +6 -0
- metadata +82 -4
- data/lib/image_util/filter/dither.rb +0 -96
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
class Color < Array
|
5
|
+
CSS_COLORS = {
|
6
|
+
"aliceblue" => [240, 248, 255],
|
7
|
+
"antiquewhite" => [250, 235, 215],
|
8
|
+
"aqua" => [0, 255, 255],
|
9
|
+
"aquamarine" => [127, 255, 212],
|
10
|
+
"azure" => [240, 255, 255],
|
11
|
+
"beige" => [245, 245, 220],
|
12
|
+
"bisque" => [255, 228, 196],
|
13
|
+
"black" => [0, 0, 0],
|
14
|
+
"blanchedalmond" => [255, 235, 205],
|
15
|
+
"blue" => [0, 0, 255],
|
16
|
+
"blueviolet" => [138, 43, 226],
|
17
|
+
"brown" => [165, 42, 42],
|
18
|
+
"burlywood" => [222, 184, 135],
|
19
|
+
"cadetblue" => [95, 158, 160],
|
20
|
+
"chartreuse" => [127, 255, 0],
|
21
|
+
"chocolate" => [210, 105, 30],
|
22
|
+
"coral" => [255, 127, 80],
|
23
|
+
"cornflowerblue" => [100, 149, 237],
|
24
|
+
"cornsilk" => [255, 248, 220],
|
25
|
+
"crimson" => [220, 20, 60],
|
26
|
+
"cyan" => [0, 255, 255],
|
27
|
+
"darkblue" => [0, 0, 139],
|
28
|
+
"darkcyan" => [0, 139, 139],
|
29
|
+
"darkgoldenrod" => [184, 134, 11],
|
30
|
+
"darkgray" => [169, 169, 169],
|
31
|
+
"darkgrey" => [169, 169, 169],
|
32
|
+
"darkgreen" => [0, 100, 0],
|
33
|
+
"darkkhaki" => [189, 183, 107],
|
34
|
+
"darkmagenta" => [139, 0, 139],
|
35
|
+
"darkolivegreen" => [85, 107, 47],
|
36
|
+
"darkorange" => [255, 140, 0],
|
37
|
+
"darkorchid" => [153, 50, 204],
|
38
|
+
"darkred" => [139, 0, 0],
|
39
|
+
"darksalmon" => [233, 150, 122],
|
40
|
+
"darkseagreen" => [143, 188, 143],
|
41
|
+
"darkslateblue" => [72, 61, 139],
|
42
|
+
"darkslategray" => [47, 79, 79],
|
43
|
+
"darkslategrey" => [47, 79, 79],
|
44
|
+
"darkturquoise" => [0, 206, 209],
|
45
|
+
"darkviolet" => [148, 0, 211],
|
46
|
+
"deeppink" => [255, 20, 147],
|
47
|
+
"deepskyblue" => [0, 191, 255],
|
48
|
+
"dimgray" => [105, 105, 105],
|
49
|
+
"dimgrey" => [105, 105, 105],
|
50
|
+
"dodgerblue" => [30, 144, 255],
|
51
|
+
"firebrick" => [178, 34, 34],
|
52
|
+
"floralwhite" => [255, 250, 240],
|
53
|
+
"forestgreen" => [34, 139, 34],
|
54
|
+
"fuchsia" => [255, 0, 255],
|
55
|
+
"gainsboro" => [220, 220, 220],
|
56
|
+
"ghostwhite" => [248, 248, 255],
|
57
|
+
"gold" => [255, 215, 0],
|
58
|
+
"goldenrod" => [218, 165, 32],
|
59
|
+
"gray" => [128, 128, 128],
|
60
|
+
"grey" => [128, 128, 128],
|
61
|
+
"green" => [0, 128, 0],
|
62
|
+
"greenyellow" => [173, 255, 47],
|
63
|
+
"honeydew" => [240, 255, 240],
|
64
|
+
"hotpink" => [255, 105, 180],
|
65
|
+
"indianred" => [205, 92, 92],
|
66
|
+
"indigo" => [75, 0, 130],
|
67
|
+
"ivory" => [255, 255, 240],
|
68
|
+
"khaki" => [240, 230, 140],
|
69
|
+
"lavender" => [230, 230, 250],
|
70
|
+
"lavenderblush" => [255, 240, 245],
|
71
|
+
"lawngreen" => [124, 252, 0],
|
72
|
+
"lemonchiffon" => [255, 250, 205],
|
73
|
+
"lightblue" => [173, 216, 230],
|
74
|
+
"lightcoral" => [240, 128, 128],
|
75
|
+
"lightcyan" => [224, 255, 255],
|
76
|
+
"lightgoldenrodyellow" => [250, 250, 210],
|
77
|
+
"lightgray" => [211, 211, 211],
|
78
|
+
"lightgrey" => [211, 211, 211],
|
79
|
+
"lightgreen" => [144, 238, 144],
|
80
|
+
"lightpink" => [255, 182, 193],
|
81
|
+
"lightsalmon" => [255, 160, 122],
|
82
|
+
"lightseagreen" => [32, 178, 170],
|
83
|
+
"lightskyblue" => [135, 206, 250],
|
84
|
+
"lightslategray" => [119, 136, 153],
|
85
|
+
"lightslategrey" => [119, 136, 153],
|
86
|
+
"lightsteelblue" => [176, 196, 222],
|
87
|
+
"lightyellow" => [255, 255, 224],
|
88
|
+
"lime" => [0, 255, 0],
|
89
|
+
"limegreen" => [50, 205, 50],
|
90
|
+
"linen" => [250, 240, 230],
|
91
|
+
"magenta" => [255, 0, 255],
|
92
|
+
"maroon" => [128, 0, 0],
|
93
|
+
"mediumaquamarine" => [102, 205, 170],
|
94
|
+
"mediumblue" => [0, 0, 205],
|
95
|
+
"mediumorchid" => [186, 85, 211],
|
96
|
+
"mediumpurple" => [147, 112, 219],
|
97
|
+
"mediumseagreen" => [60, 179, 113],
|
98
|
+
"mediumslateblue" => [123, 104, 238],
|
99
|
+
"mediumspringgreen" => [0, 250, 154],
|
100
|
+
"mediumturquoise" => [72, 209, 204],
|
101
|
+
"mediumvioletred" => [199, 21, 133],
|
102
|
+
"midnightblue" => [25, 25, 112],
|
103
|
+
"mintcream" => [245, 255, 250],
|
104
|
+
"mistyrose" => [255, 228, 225],
|
105
|
+
"moccasin" => [255, 228, 181],
|
106
|
+
"navajowhite" => [255, 222, 173],
|
107
|
+
"navy" => [0, 0, 128],
|
108
|
+
"oldlace" => [253, 245, 230],
|
109
|
+
"olive" => [128, 128, 0],
|
110
|
+
"olivedrab" => [107, 142, 35],
|
111
|
+
"orange" => [255, 165, 0],
|
112
|
+
"orangered" => [255, 69, 0],
|
113
|
+
"orchid" => [218, 112, 214],
|
114
|
+
"palegoldenrod" => [238, 232, 170],
|
115
|
+
"palegreen" => [152, 251, 152],
|
116
|
+
"paleturquoise" => [175, 238, 238],
|
117
|
+
"palevioletred" => [219, 112, 147],
|
118
|
+
"papayawhip" => [255, 239, 213],
|
119
|
+
"peachpuff" => [255, 218, 185],
|
120
|
+
"peru" => [205, 133, 63],
|
121
|
+
"pink" => [255, 192, 203],
|
122
|
+
"plum" => [221, 160, 221],
|
123
|
+
"powderblue" => [176, 224, 230],
|
124
|
+
"purple" => [128, 0, 128],
|
125
|
+
"red" => [255, 0, 0],
|
126
|
+
"rosybrown" => [188, 143, 143],
|
127
|
+
"royalblue" => [65, 105, 225],
|
128
|
+
"saddlebrown" => [139, 69, 19],
|
129
|
+
"salmon" => [250, 128, 114],
|
130
|
+
"sandybrown" => [244, 164, 96],
|
131
|
+
"seagreen" => [46, 139, 87],
|
132
|
+
"seashell" => [255, 245, 238],
|
133
|
+
"sienna" => [160, 82, 45],
|
134
|
+
"silver" => [192, 192, 192],
|
135
|
+
"skyblue" => [135, 206, 235],
|
136
|
+
"slateblue" => [106, 90, 205],
|
137
|
+
"slategray" => [112, 128, 144],
|
138
|
+
"slategrey" => [112, 128, 144],
|
139
|
+
"snow" => [255, 250, 250],
|
140
|
+
"springgreen" => [0, 255, 127],
|
141
|
+
"steelblue" => [70, 130, 180],
|
142
|
+
"tan" => [210, 180, 140],
|
143
|
+
"teal" => [0, 128, 128],
|
144
|
+
"thistle" => [216, 191, 216],
|
145
|
+
"tomato" => [255, 99, 71],
|
146
|
+
"turquoise" => [64, 224, 208],
|
147
|
+
"violet" => [238, 130, 238],
|
148
|
+
"wheat" => [245, 222, 179],
|
149
|
+
"white" => [255, 255, 255],
|
150
|
+
"whitesmoke" => [245, 245, 245],
|
151
|
+
"yellow" => [255, 255, 0],
|
152
|
+
"yellowgreen" => [154, 205, 50],
|
153
|
+
"rebeccapurple" => [102, 51, 153]
|
154
|
+
}.transform_values(&:freeze).transform_keys(&:to_sym).freeze
|
155
|
+
|
156
|
+
CSS_COLORS_4C = CSS_COLORS.transform_values { |i| (i + [255]).freeze }.freeze
|
157
|
+
end
|
158
|
+
end
|
data/lib/image_util/color.rb
CHANGED
@@ -6,6 +6,7 @@ module ImageUtil
|
|
6
6
|
super(args)
|
7
7
|
end
|
8
8
|
|
9
|
+
autoload :CSS_COLORS, "image_util/color/css_colors"
|
9
10
|
def r = self[0]
|
10
11
|
def g = self[1]
|
11
12
|
def b = self[2]
|
@@ -40,7 +41,7 @@ module ImageUtil
|
|
40
41
|
end.then { |val| new(*val) }
|
41
42
|
end
|
42
43
|
|
43
|
-
def to_buffer(color_bits,
|
44
|
+
def to_buffer(color_bits, channels)
|
44
45
|
map do |i|
|
45
46
|
case color_bits
|
46
47
|
when 8
|
@@ -48,9 +49,47 @@ module ImageUtil
|
|
48
49
|
else
|
49
50
|
(i.to_f * 2**(color_bits - 8)).to_i
|
50
51
|
end
|
51
|
-
end + [255] * (
|
52
|
+
end + [255] * (channels - length)
|
52
53
|
end
|
53
54
|
|
55
|
+
# rubocop:disable Metrics/BlockNesting
|
56
|
+
|
57
|
+
# Optimized shortpath for a heavily hit fragment. Let's skip creating colors if
|
58
|
+
# they are to be output to buffer instantly.
|
59
|
+
def self.from_any_to_buffer(value, color_bits, channels)
|
60
|
+
if color_bits == 8
|
61
|
+
case value
|
62
|
+
when Color
|
63
|
+
return value.to_buffer(color_bits, channels)
|
64
|
+
when Array
|
65
|
+
if channels == value.length && value.all? { |i| nice_int?(i) }
|
66
|
+
return value
|
67
|
+
elsif channels == 4 && value.length == 3 && value.all? { |i| nice_int?(i) }
|
68
|
+
return value + [255]
|
69
|
+
end
|
70
|
+
when Symbol, String
|
71
|
+
s = value.to_sym
|
72
|
+
if CSS_COLORS.key?(s)
|
73
|
+
if channels == 3
|
74
|
+
return CSS_COLORS[s]
|
75
|
+
elsif channels == 4
|
76
|
+
return CSS_COLORS_4C[s]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
from(value).to_buffer(color_bits, channels)
|
82
|
+
end
|
83
|
+
|
84
|
+
# rubocop:enable Metrics/BlockNesting
|
85
|
+
# rubocop:disable Style/ClassEqualityComparison
|
86
|
+
|
87
|
+
def self.nice_int?(i)
|
88
|
+
i.class == Integer && i >= 0 && i <= 255
|
89
|
+
end
|
90
|
+
|
91
|
+
# rubocop:enable Style/ClassEqualityComparison
|
92
|
+
|
54
93
|
def self.from(value)
|
55
94
|
case value
|
56
95
|
when Color
|
@@ -69,17 +108,20 @@ module ImageUtil
|
|
69
108
|
new($1.to_i(16), $2.to_i(16), $3.to_i(16))
|
70
109
|
when /\A#(\h{2})(\h{2})(\h{2})(\h{2})\z/
|
71
110
|
new($1.to_i(16), $2.to_i(16), $3.to_i(16), $4.to_i(16))
|
72
|
-
when "black" then new(0, 0, 0)
|
73
|
-
when "white" then new(255, 255, 255)
|
74
|
-
when "red" then new(255, 0, 0)
|
75
|
-
when "lime" then new(0, 255, 0)
|
76
|
-
when "blue" then new(0, 0, 255)
|
77
111
|
else
|
78
|
-
|
112
|
+
if (rgb = CSS_COLORS[value.downcase.to_sym])
|
113
|
+
new(*rgb)
|
114
|
+
else
|
115
|
+
raise ArgumentError, "wrong String passed as color (passed: #{value.inspect})"
|
116
|
+
end
|
79
117
|
end
|
80
118
|
when Symbol
|
81
|
-
|
82
|
-
|
119
|
+
if (rgb = CSS_COLORS[value])
|
120
|
+
new(*rgb)
|
121
|
+
else
|
122
|
+
from(value.to_s)
|
123
|
+
end
|
124
|
+
when Integer, Float, NilClass
|
83
125
|
new(*[component_from_number(value)] * 3)
|
84
126
|
else
|
85
127
|
raise ArgumentError, "wrong type passed as color (passed: #{value.inspect})"
|
@@ -139,11 +181,22 @@ module ImageUtil
|
|
139
181
|
Color.new(out_r, out_g, out_b, out_a * 255)
|
140
182
|
end
|
141
183
|
|
142
|
-
#
|
184
|
+
# If given a Numeric argument, multiplies the alpha channel by the given factor
|
185
|
+
# and returns a new color.
|
186
|
+
#
|
187
|
+
# If given a color, create a new one by multiplying all channels.
|
143
188
|
def *(other)
|
144
|
-
|
145
|
-
|
146
|
-
|
189
|
+
case other
|
190
|
+
when Numeric
|
191
|
+
Color.new(r, g, b, (a * other).clamp(0, 255))
|
192
|
+
when Color
|
193
|
+
Color.new(*map.with_index do |c, idx|
|
194
|
+
oc = other[idx] || (idx == 3 ? 255 : 0)
|
195
|
+
c * oc / 255
|
196
|
+
end)
|
197
|
+
else
|
198
|
+
raise TypeError, "factor must be either numeric or color"
|
199
|
+
end
|
147
200
|
end
|
148
201
|
end
|
149
202
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
module Extension
|
5
|
+
EXTENSIONS = {
|
6
|
+
pam: [".pam"],
|
7
|
+
png: [".png"],
|
8
|
+
jpeg: [".jpg", ".jpeg"],
|
9
|
+
gif: [".gif"],
|
10
|
+
apng: [".apng"]
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
LOOKUP = EXTENSIONS.flat_map { |fmt, exts| exts.map { |e| [e, fmt] } }.to_h.freeze
|
14
|
+
|
15
|
+
module_function
|
16
|
+
|
17
|
+
def detect(path)
|
18
|
+
return nil unless path
|
19
|
+
|
20
|
+
ext = File.extname(path.to_s).downcase
|
21
|
+
LOOKUP[ext]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -4,15 +4,15 @@ module ImageUtil
|
|
4
4
|
module Filter
|
5
5
|
module Background
|
6
6
|
def background(bgcolor)
|
7
|
-
return self if
|
7
|
+
return self if channels == 3
|
8
8
|
|
9
|
-
unless
|
9
|
+
unless channels == 4
|
10
10
|
raise ArgumentError, "background only supported on RGB or RGBA images"
|
11
11
|
end
|
12
12
|
|
13
13
|
bg = Color.from(bgcolor)
|
14
|
-
img = Image.new(*dimensions, color_bits: color_bits,
|
15
|
-
img.set_each_pixel_by_location do |loc|
|
14
|
+
img = Image.new(*dimensions, color_bits: color_bits, channels: 3)
|
15
|
+
img.set_each_pixel_by_location! do |loc|
|
16
16
|
over = bg + self[*loc]
|
17
17
|
Color.new(over.r, over.g, over.b)
|
18
18
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
module Filter
|
5
|
+
module BitmapText
|
6
|
+
extend ImageUtil::Filter::Mixin
|
7
|
+
|
8
|
+
def bitmap_text!(text, *location, **kwargs)
|
9
|
+
loc = location.dup
|
10
|
+
loc += [0] * (dimensions.length - loc.length)
|
11
|
+
paste!(Image.bitmap_text(text, **kwargs), *loc, respect_alpha: true)
|
12
|
+
end
|
13
|
+
|
14
|
+
define_immutable_version :bitmap_text
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
module Filter
|
5
|
+
module Colors
|
6
|
+
extend ImageUtil::Filter::Mixin
|
7
|
+
|
8
|
+
def color_multiply!(color)
|
9
|
+
col = Color.from(color)
|
10
|
+
set_each_pixel_by_location! do |loc|
|
11
|
+
self[*loc] * col
|
12
|
+
end
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
define_immutable_version :color_multiply
|
17
|
+
|
18
|
+
alias * color_multiply
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -16,12 +16,12 @@ module ImageUtil
|
|
16
16
|
)
|
17
17
|
fp = self.view(view)
|
18
18
|
|
19
|
-
axis = axis_to_number(axis)
|
19
|
+
axis = Filter::Mixin.axis_to_number(axis)
|
20
20
|
draw_axis ||= case axis
|
21
21
|
when 0 then 1
|
22
22
|
when 1 then 0
|
23
23
|
end
|
24
|
-
draw_axis = axis_to_number(draw_axis)
|
24
|
+
draw_axis = Filter::Mixin.axis_to_number(draw_axis)
|
25
25
|
|
26
26
|
limit ||= (0..)
|
27
27
|
limit = Range.new(limit.begin, dimensions[axis]-1, false) if limit.end == nil
|
@@ -62,16 +62,29 @@ module ImageUtil
|
|
62
62
|
end
|
63
63
|
end
|
64
64
|
|
65
|
-
|
65
|
+
def draw_circle!(center, radius, color = Color[:black], view: View::Interpolated)
|
66
|
+
fp = self.view(view)
|
67
|
+
cx, cy = center
|
68
|
+
min_x = (cx - radius).ceil
|
69
|
+
max_x = (cx + radius).floor
|
70
|
+
min_x.upto(max_x) do |x|
|
71
|
+
dy = Math.sqrt(radius * radius - (x - cx)**2)
|
72
|
+
fp[x, cy + dy] = color
|
73
|
+
fp[x, cy - dy] = color
|
74
|
+
end
|
66
75
|
|
67
|
-
|
76
|
+
min_y = (cy - radius).ceil
|
77
|
+
max_y = (cy + radius).floor
|
78
|
+
min_y.upto(max_y) do |y|
|
79
|
+
dx = Math.sqrt(radius * radius - (y - cy)**2)
|
80
|
+
fp[cx + dx, y] = color
|
81
|
+
fp[cx - dx, y] = color
|
82
|
+
end
|
68
83
|
|
69
|
-
|
70
|
-
axis = 0 if axis == :x
|
71
|
-
axis = 1 if axis == :y
|
72
|
-
axis = 2 if axis == :z
|
73
|
-
axis
|
84
|
+
self
|
74
85
|
end
|
86
|
+
|
87
|
+
define_immutable_version :draw_function, :draw_line, :draw_circle
|
75
88
|
end
|
76
89
|
end
|
77
90
|
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
module Filter
|
5
|
+
module Palette
|
6
|
+
extend ImageUtil::Filter::Mixin
|
7
|
+
|
8
|
+
# A more descriptive name for this structure would be
|
9
|
+
# N-tree, where N = 2**color_components. Let's pretend
|
10
|
+
# we are dealing with 3-color component trees though,
|
11
|
+
# so Octree it is.
|
12
|
+
class ColorOctree < Array
|
13
|
+
def dig(nexthop, *rest)
|
14
|
+
self[nexthop] ||= ColorOctree.new(length)
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
def number_bits(number)
|
19
|
+
array = Array.new(8)
|
20
|
+
i = 0
|
21
|
+
number = number.to_i
|
22
|
+
while i < 8
|
23
|
+
array[7 - i] = ((number >> i) & 1)
|
24
|
+
i += 1
|
25
|
+
end
|
26
|
+
array
|
27
|
+
end
|
28
|
+
|
29
|
+
def generate_key(component_bits)
|
30
|
+
number = 0
|
31
|
+
component_bits.reverse_each do |i|
|
32
|
+
number <<= 1
|
33
|
+
number |= i
|
34
|
+
end
|
35
|
+
number
|
36
|
+
end
|
37
|
+
|
38
|
+
def key_array(color)
|
39
|
+
color.map do |component|
|
40
|
+
number_bits(component)
|
41
|
+
end.transpose.map do |i|
|
42
|
+
generate_key(i)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# rubocop:disable Style/Semicolon
|
47
|
+
|
48
|
+
# Optimized path for (r,g,b) colors.
|
49
|
+
def key_array3(color)
|
50
|
+
i = 0
|
51
|
+
r = color[0].to_i; g = color[1].to_i; b = color[2].to_i
|
52
|
+
array = Array.new(8)
|
53
|
+
while i < 8
|
54
|
+
array[7 - i] = (r & 1) | (g & 1) << 1 | (b & 1) << 2
|
55
|
+
r >>= 1; g >>= 1; b >>= 1
|
56
|
+
i += 1
|
57
|
+
end
|
58
|
+
array
|
59
|
+
end
|
60
|
+
|
61
|
+
# Optimized path for (r,g,b,a) colors.
|
62
|
+
def key_array4(color)
|
63
|
+
i = 0
|
64
|
+
r = color[0].to_i; g = color[1].to_i; b = color[2].to_i; a = color[3].to_i
|
65
|
+
array = Array.new(8)
|
66
|
+
while i < 8
|
67
|
+
array[7 - i] = (r & 1) | (g & 1) << 1 | (b & 1) << 2 | (a & 1) << 3
|
68
|
+
r >>= 1; g >>= 1; b >>= 1; a >>= 1
|
69
|
+
i += 1
|
70
|
+
end
|
71
|
+
array
|
72
|
+
end
|
73
|
+
|
74
|
+
# rubocop:enable Style/Semicolon
|
75
|
+
|
76
|
+
def build_from(colors)
|
77
|
+
colors.each do |color|
|
78
|
+
color_path = case color.length
|
79
|
+
when 3
|
80
|
+
key_array3(color)
|
81
|
+
when 4
|
82
|
+
key_array4(color)
|
83
|
+
else
|
84
|
+
key_array(color)
|
85
|
+
end
|
86
|
+
|
87
|
+
dig(*color_path[0..-2])[color_path[-1]] = color
|
88
|
+
end
|
89
|
+
|
90
|
+
self
|
91
|
+
end
|
92
|
+
|
93
|
+
# rubocop:disable Style/StringConcatenation
|
94
|
+
def inspect
|
95
|
+
"#<Octree:[" +
|
96
|
+
filter_map.with_index do |i,idx|
|
97
|
+
"0b#{idx.to_s(2)} => #{i.inspect}," if i
|
98
|
+
end.join(", ") +
|
99
|
+
"]>"
|
100
|
+
end
|
101
|
+
# rubocop:enable Style/StringConcatenation
|
102
|
+
|
103
|
+
def available_with_index
|
104
|
+
each_with_index.select(&:first)
|
105
|
+
end
|
106
|
+
|
107
|
+
def pretty_print(pp)
|
108
|
+
pp.group(1, "#<Octree:", ">") do
|
109
|
+
pp.breakable ""
|
110
|
+
pp.group(1, "[", "]") do
|
111
|
+
pp.seplist(available_with_index) do |i,idx|
|
112
|
+
pp.text "0b#{idx.to_s(2)}"
|
113
|
+
pp.text " => "
|
114
|
+
pp.pp i
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def empty?
|
121
|
+
compact.empty?
|
122
|
+
end
|
123
|
+
|
124
|
+
def take(n)
|
125
|
+
taken = []
|
126
|
+
length.times do |idx|
|
127
|
+
next unless (i = self[idx])
|
128
|
+
|
129
|
+
taken << i
|
130
|
+
self[idx] = nil
|
131
|
+
|
132
|
+
if taken.length >= n - 1 && !empty?
|
133
|
+
taken << self
|
134
|
+
break
|
135
|
+
elsif taken.length >= n
|
136
|
+
break
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
raise if taken.length > n
|
141
|
+
|
142
|
+
taken
|
143
|
+
end
|
144
|
+
|
145
|
+
def colors
|
146
|
+
select { |i| i.is_a? Color } +
|
147
|
+
select { |i| i.is_a? ColorOctree }.map { |i| i.colors }.flatten(1)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def palette_reduce!(count)
|
152
|
+
colors = unique_colors
|
153
|
+
|
154
|
+
return self if colors.length <= count
|
155
|
+
|
156
|
+
octree = ColorOctree.new
|
157
|
+
octree.build_from(colors)
|
158
|
+
|
159
|
+
queue = [octree]
|
160
|
+
while queue.length < count
|
161
|
+
elem = queue.shift
|
162
|
+
case elem
|
163
|
+
when Color
|
164
|
+
queue << elem
|
165
|
+
when ColorOctree
|
166
|
+
needed = count - queue.length
|
167
|
+
got = elem.take(needed)
|
168
|
+
queue += got
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
equiv = {}
|
173
|
+
|
174
|
+
queue.each do |i|
|
175
|
+
case i
|
176
|
+
when Color
|
177
|
+
equiv[i] = i
|
178
|
+
when ColorOctree
|
179
|
+
colors = i.colors
|
180
|
+
picked = colors[colors.length / 2]
|
181
|
+
colors.each do |color|
|
182
|
+
equiv[color] = picked
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
set_each_pixel_by_location! do |loc|
|
188
|
+
equiv[self[*loc]]
|
189
|
+
end
|
190
|
+
|
191
|
+
self
|
192
|
+
end
|
193
|
+
|
194
|
+
define_immutable_version :palette_reduce
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|