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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +5 -6
  3. data/CHANGELOG.md +41 -6
  4. data/README.md +229 -81
  5. data/Rakefile +5 -0
  6. data/docs/cli.md +5 -0
  7. data/docs/samples/background.png +0 -0
  8. data/docs/samples/bitmap_text.png +0 -0
  9. data/docs/samples/colors.png +0 -0
  10. data/docs/samples/constructor.png +0 -0
  11. data/docs/samples/dither.png +0 -0
  12. data/docs/samples/draw.png +0 -0
  13. data/docs/samples/iterator.png +0 -0
  14. data/docs/samples/paste.png +0 -0
  15. data/docs/samples/pdither.png +0 -0
  16. data/docs/samples/pipe.png +0 -0
  17. data/docs/samples/range.png +0 -0
  18. data/docs/samples/redimension.png +0 -0
  19. data/docs/samples/resize.png +0 -0
  20. data/docs/samples/sixel.png +0 -0
  21. data/docs/samples/transform.png +0 -0
  22. data/exe/image_util +7 -0
  23. data/lib/image_util/benchmarking.rb +25 -0
  24. data/lib/image_util/bitmap_font/fonts/smfont/charset.txt +1 -0
  25. data/lib/image_util/bitmap_font/fonts/smfont/font.png +0 -0
  26. data/lib/image_util/bitmap_font.rb +72 -0
  27. data/lib/image_util/cli.rb +54 -0
  28. data/lib/image_util/codec/chunky_png.rb +67 -0
  29. data/lib/image_util/codec/image_magick.rb +82 -15
  30. data/lib/image_util/codec/kitty.rb +81 -0
  31. data/lib/image_util/codec/libpng.rb +2 -10
  32. data/lib/image_util/codec/libsixel.rb +14 -14
  33. data/lib/image_util/codec/libturbojpeg.rb +1 -11
  34. data/lib/image_util/codec/pam.rb +24 -22
  35. data/lib/image_util/codec/ruby_sixel.rb +12 -13
  36. data/lib/image_util/codec.rb +5 -1
  37. data/lib/image_util/color/css_colors.rb +158 -0
  38. data/lib/image_util/color.rb +67 -14
  39. data/lib/image_util/extension.rb +24 -0
  40. data/lib/image_util/filter/_mixin.rb +9 -0
  41. data/lib/image_util/filter/background.rb +4 -4
  42. data/lib/image_util/filter/bitmap_text.rb +17 -0
  43. data/lib/image_util/filter/colors.rb +21 -0
  44. data/lib/image_util/filter/draw.rb +22 -9
  45. data/lib/image_util/filter/palette.rb +197 -0
  46. data/lib/image_util/filter/paste.rb +1 -1
  47. data/lib/image_util/filter/redimension.rb +83 -0
  48. data/lib/image_util/filter/resize.rb +1 -1
  49. data/lib/image_util/filter/transform.rb +48 -0
  50. data/lib/image_util/filter.rb +5 -1
  51. data/lib/image_util/generator/bitmap_text.rb +38 -0
  52. data/lib/image_util/generator/example/rose.png +0 -0
  53. data/lib/image_util/generator/example.rb +9 -0
  54. data/lib/image_util/generator.rb +8 -0
  55. data/lib/image_util/image/buffer.rb +11 -11
  56. data/lib/image_util/image.rb +54 -26
  57. data/lib/image_util/magic.rb +8 -6
  58. data/lib/image_util/statistic/{color.rb → colors.rb} +2 -2
  59. data/lib/image_util/statistic.rb +1 -1
  60. data/lib/image_util/terminal.rb +61 -0
  61. data/lib/image_util/version.rb +1 -1
  62. data/lib/image_util/view/interpolated.rb +1 -1
  63. data/lib/image_util.rb +6 -0
  64. metadata +82 -4
  65. 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
@@ -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, color_length)
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] * (color_length - length)
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
- raise ArgumentError, "wrong String passed as color (passed: #{value.inspect})"
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
- from(value.to_s)
82
- when Integer, Float, nil
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
- # Multiplies the alpha channel by the given factor and returns a new color.
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
- raise TypeError, "factor must be numeric" unless other.is_a?(Numeric)
145
-
146
- Color.new(r, g, b, (a * other).clamp(0, 255))
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
@@ -10,6 +10,15 @@ module ImageUtil
10
10
  end
11
11
  end
12
12
  end
13
+
14
+ def axis_to_number(axis)
15
+ axis = 0 if axis == :x
16
+ axis = 1 if axis == :y
17
+ axis = 2 if axis == :z
18
+ axis
19
+ end
20
+
21
+ module_function :axis_to_number
13
22
  end
14
23
  end
15
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 color_length == 3
7
+ return self if channels == 3
8
8
 
9
- unless color_length == 4
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, color_length: 3)
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
- define_immutable_version :draw_function, :draw_line
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
- private
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
- def axis_to_number(axis)
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
@@ -11,7 +11,7 @@ module ImageUtil
11
11
  if !respect_alpha &&
12
12
  image.dimensions.length == 1 &&
13
13
  image.color_bits == color_bits &&
14
- image.color_length == color_length &&
14
+ image.channels == channels &&
15
15
  buffer.respond_to?(:copy_1d)
16
16
  loc = location.map(&:to_i)
17
17
  begin