image_util 0.2.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +3 -0
  3. data/CHANGELOG.md +31 -0
  4. data/README.md +125 -24
  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/iterator.png +0 -0
  13. data/docs/samples/pdither.png +0 -0
  14. data/docs/samples/pipe.png +0 -0
  15. data/docs/samples/range.png +0 -0
  16. data/docs/samples/redimension.png +0 -0
  17. data/docs/samples/resize.png +0 -0
  18. data/docs/samples/transform.png +0 -0
  19. data/exe/image_util +7 -0
  20. data/lib/image_util/benchmarking.rb +25 -0
  21. data/lib/image_util/bitmap_font/fonts/smfont/charset.txt +1 -0
  22. data/lib/image_util/bitmap_font/fonts/smfont/font.png +0 -0
  23. data/lib/image_util/bitmap_font.rb +72 -0
  24. data/lib/image_util/cli.rb +54 -0
  25. data/lib/image_util/codec/chunky_png.rb +67 -0
  26. data/lib/image_util/codec/image_magick.rb +76 -18
  27. data/lib/image_util/codec/kitty.rb +81 -0
  28. data/lib/image_util/codec/libpng.rb +2 -10
  29. data/lib/image_util/codec/libsixel.rb +14 -14
  30. data/lib/image_util/codec/libturbojpeg.rb +1 -11
  31. data/lib/image_util/codec/pam.rb +24 -22
  32. data/lib/image_util/codec/ruby_sixel.rb +11 -12
  33. data/lib/image_util/codec.rb +5 -1
  34. data/lib/image_util/color/css_colors.rb +3 -1
  35. data/lib/image_util/color.rb +62 -9
  36. data/lib/image_util/extension.rb +24 -0
  37. data/lib/image_util/filter/_mixin.rb +9 -0
  38. data/lib/image_util/filter/background.rb +4 -4
  39. data/lib/image_util/filter/bitmap_text.rb +17 -0
  40. data/lib/image_util/filter/colors.rb +21 -0
  41. data/lib/image_util/filter/draw.rb +2 -11
  42. data/lib/image_util/filter/palette.rb +197 -0
  43. data/lib/image_util/filter/paste.rb +1 -1
  44. data/lib/image_util/filter/redimension.rb +83 -0
  45. data/lib/image_util/filter/resize.rb +1 -1
  46. data/lib/image_util/filter/transform.rb +48 -0
  47. data/lib/image_util/filter.rb +5 -1
  48. data/lib/image_util/generator/bitmap_text.rb +38 -0
  49. data/lib/image_util/generator/example/rose.png +0 -0
  50. data/lib/image_util/generator/example.rb +9 -0
  51. data/lib/image_util/generator.rb +8 -0
  52. data/lib/image_util/image/buffer.rb +11 -11
  53. data/lib/image_util/image.rb +49 -23
  54. data/lib/image_util/magic.rb +8 -6
  55. data/lib/image_util/statistic/{color.rb → colors.rb} +2 -2
  56. data/lib/image_util/statistic.rb +1 -1
  57. data/lib/image_util/terminal.rb +61 -0
  58. data/lib/image_util/version.rb +1 -1
  59. data/lib/image_util/view/interpolated.rb +1 -1
  60. data/lib/image_util.rb +6 -0
  61. metadata +75 -4
  62. data/lib/image_util/filter/dither.rb +0 -96
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92f9a0c088385d833ceee58b203a3ba52a4de3d2cbfdd16ed6dc8d0213e18753
4
- data.tar.gz: 275a94eb28e30c8d1a59ba25723284e864e8ad9b8a1041826cba37d983ba01fd
3
+ metadata.gz: 31687d0660e040e340b6ed26f581f95de0746e33f992acf7dfcdb4fa4a88f4cd
4
+ data.tar.gz: 4fee3793295fb2207a54caa1ed430bdc43ddc8307f96ef597ba0ed777618b8e6
5
5
  SHA512:
6
- metadata.gz: 523748193e1b51083729b1583cfec3ac19ec02a11e893cc3d858deb30388eebd013a01ade2770cd236c2f947bffc0cc2583783d3094552451d99b873d840311b
7
- data.tar.gz: 228f82c3508d236822c4a6e27ad721381ffa42aaa31d1e3ab839eeaf9d60363ab2bcd7e6340fab00cdeac23320ad4681036187bd2eb5bb5680242ff9a8451464
6
+ metadata.gz: c02064cb968adae5e5d5b5328922e77fd542d6ca4322a9259d27b513339ff44ca71e272f8cf01055d864f3dbc3fef702d3187a745e98af09a95f2dc9907470c4
7
+ data.tar.gz: 97c9852b31aab0d2b71f19579968d856fd87eed7339c3b2ec9e6bd1dcee3b674862821f6d4a033148db8d45b0a8228d890670d43c2f0ba921dc98d7d5ed774de
data/AGENTS.md CHANGED
@@ -10,3 +10,6 @@
10
10
  - Don't discuss codec internals or bug fixes in README. Only list supported formats. Document bug fixes in the CHANGELOG, not in README.
11
11
  - Specs target at least 80% coverage as enforced by SimpleCov.
12
12
  - The library aims to remain lightweight and portable.
13
+ - Remember to always ensure rake tests pass and Rubocop doesn't complain.
14
+ - If you are an OpenAI Codex, don't upload images! Tell me to use `rebuild-images-in-readme` script.
15
+ - When adding files into collection directories (like codec/), if something isn't part of a collection (ie. isn't a codec, but is a mixin), ensure to name the file like `_something.rb`. Consult existing directory structure.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,34 @@
1
+ ## [0.3.0] - 2025-07-25
2
+ - Rename `dither!` to `palette_reduce!`
3
+ - Rename `#set_each_pixel_by_location` to `#set_each_pixel_by_location!` since it's mutable
4
+ - Rename `color_length` to a more appropriate name `channels`
5
+ - Thor based CLI with a `support` command that lists codec support, default
6
+ format handlers and detected terminal features
7
+ - Replace `palette_reduce!` implementation with a much faster one
8
+ - Terminal detection for graphic protocols
9
+ - Support Kitty graphics protocol
10
+ - Transform filter with rotate and flip operations
11
+ - Fallback PNG codec via chunky_png
12
+ - ImageMagick codec can now read and write `gif` and `apng` including animations
13
+ - Redimension filter to change image dimensions
14
+ - Sixel and Kitty codecs accept 1D images
15
+ - `Pam.encode` no longer accepts `fill_to`; Sixel codecs pad images using the
16
+ redimension filter
17
+ - Add `BitmapFont` with a sample hand crafted font, add `bitmap_text` generator.
18
+ - `Color#*` can now accept another `Color` to multiply channels
19
+ - Add `Colors` filter with `color_multiply!` and alias `*`
20
+ - `bitmap_text` accepts multiline strings and supports colorization
21
+ - `bitmap_text` supports left, center and right alignment
22
+ - Add `BitmapText` filter for overlaying text onto images
23
+ - `bitmap_text` filter now always respects an alpha channel when pasting text
24
+ - Open ImageMagick codec pipes in binary mode for Windows compatibility
25
+ - Format inference from file extension in `Image#to_file`
26
+ - ImageMagick codec now reads PAM frames using the Pam codec
27
+ - Force 8-bit output when decoding through ImageMagick to avoid 1-bit images on Windows
28
+ - ImageMagick codec checks available formats before advertising APNG support
29
+ - Add `Example` generator: `Image.example_rose`
30
+ - Disable APNG on Windows until we find out something else than ImageMagick to support it
31
+
1
32
  ## [0.2.0] - 2025-07-21
2
33
  - Ruby Sixel encoder now sets pixel aspect ratio metadata to display correctly in Windows Terminal
3
34
  - Support for all CSS color names
data/README.md CHANGED
@@ -14,34 +14,59 @@ img = ImageUtil::Image.new(40, 40)
14
14
  An optional block receives pixel coordinates and should return something that can be converted to a color. Dimensions of more than two axes are supported.
15
15
 
16
16
  ```ruby
17
- img = ImageUtil::Image.new(4, 4) { |x, y| ImageUtil::Color[x * 64, y * 64, 0] }
17
+ img = ImageUtil::Image.new(128, 128) { |x, y| ImageUtil::Color[x, y, 40] }
18
18
  ```
19
19
 
20
+ ![Constructor example](docs/samples/constructor.png)
21
+
20
22
  ## Loading and Saving
21
23
 
22
24
  Instead of building an image from scratch you can load one with
23
25
  `ImageUtil::Image.from_string` or `ImageUtil::Image.from_file`.
24
- Both helpers understand the built in codecs for `png`, `jpeg` and `pam`
25
- formats:
26
+ Both helpers understand the built in codecs for `png`, `jpeg`, `pam`, `gif`
27
+ and `apng` formats:
26
28
 
27
29
  ```ruby
28
30
  img = ImageUtil::Image.from_file("logo.png")
29
31
  data = ImageUtil::Image.from_string(File.binread("logo.jpeg"))
30
32
  ```
31
33
 
34
+ A `from_file` method also supports passing IO objects:
35
+
36
+ ```ruby
37
+ img = ImageUtil::Image.from_file(IO.popen("magick rose: png:"))
38
+ img.draw_line([0,0], [69,45], :blue)
39
+ ```
40
+
41
+ ![Pipe load example](docs/samples/pipe.png)
42
+
32
43
  The same formats can be written back using `to_string` or `to_file`.
44
+ When saving to a file path, `to_file` can infer the format from the file extension.
33
45
 
34
46
  ```ruby
35
- img.to_file("out.png", :png)
47
+ img.to_file("out.png")
36
48
  binary = img.to_string(:jpeg)
37
49
  ```
38
50
 
39
- ## SIXEL Output
51
+ ## Image Information
52
+
53
+ After loading or creating an image, you can access information about it, like
54
+ dimensions or color bits.
55
+
56
+ ```ruby
57
+ img.dimensions # => [20,30]
58
+ img.width # => 20
59
+ img.height # => 30
60
+ img.color_bits # => 8 (means every channel has 8 bits of color)
61
+ img.channels # => 3 (RGB)
62
+ ```
63
+
64
+ ## Terminal Output
40
65
 
41
66
  Images can be previewed in compatible terminals:
42
67
 
43
68
  ```ruby
44
- puts img.to_sixel
69
+ puts ImageUtil::Terminal.output_image($stdin, $stdout, img)
45
70
  ```
46
71
 
47
72
  In `irb` or `pry` the `inspect` method shows the image automatically, so you can
@@ -51,12 +76,15 @@ just evaluate the object:
51
76
  img
52
77
  ```
53
78
 
54
- Most notably, SIXEL works in Windows Terminal, Konsole (KDE), iTerm2 (macOS), XTerm (launch with: `xterm -ti vt340`). Here's how it looks in Konsole:
79
+ The library checks if the Kitty graphics protocol is available and falls back to SIXEL otherwise. Kitty graphics protocol is supported by Kitty, Konsole
80
+ and a couple others. SIXEL, most notably, works in Windows Terminal, Konsole (KDE), iTerm2 (macOS), XTerm (launch with: `xterm -ti vt340`). Here's how SIXEL
81
+ looks in Konsole:
55
82
 
56
83
  ![Sixel example](docs/samples/sixel.png)
57
84
 
85
+ This library supports generating Sixel with either `libsixel`, `ImageMagick` or using a pure-Ruby Sixel generator. For best performance, try to install one of
86
+ the earlier system packages. Both Kitty and SIXEL outputs also accept one-dimensional images, treating them as height `1`.
58
87
 
59
- This library supports generating Sixel with either `libsixel`, `ImageMagick` or using a pure-Ruby Sixel generator. For best performance, try to install one of the earlier system packages.
60
88
 
61
89
  ## Color Values
62
90
 
@@ -94,32 +122,45 @@ patch = img[0..1, 0..1]
94
122
  For instance, you can extract a region, edit it and paste it back:
95
123
 
96
124
  ```ruby
97
- img = ImageUtil::Image.new(4, 4) { [0, 0, 0] }
98
- corner = img[0..1, 0..1]
125
+ img = ImageUtil::Image.new(128, 128) { [0, 0, 0] }
126
+ corner = img[0..32, 0..32]
99
127
  corner.all = :green
100
- img[0..1, 0..1] = corner
128
+ img[0..32, 0..32] = corner
101
129
  img[2, 2] = :yellow
102
- img.to_file("pixel_patch.png", :png)
130
+ # img.to_file("pixel_patch.png", :png)
131
+ img
103
132
  ```
104
133
 
134
+ ![Range access example](docs/samples/range.png)
135
+
105
136
  Assigning an image to a range automatically resizes it to fit before pasting.
106
137
 
138
+ On the other hand, if you assign a color to a range, it will fill all referenced
139
+ pixels with that color (draw a rectangle).
140
+
107
141
  Iteration helpers operate on arbitrary ranges and share the same syntax used
108
142
  when indexing images. `each_pixel` yields color objects, while
109
- `each_pixel_location` yields coordinate arrays. `set_each_pixel_by_location`
143
+ `each_pixel_location` yields coordinate arrays. `set_each_pixel_by_location!`
110
144
  assigns the value returned by the block to every location (unless `nil` is returned).
111
145
 
112
146
  ```ruby
147
+ # create an all-black image
148
+ img = ImageUtil::Image.new(128, 128) { :black }
149
+
113
150
  # fill a checkerboard pattern
114
- img = ImageUtil::Image.new(8, 8) { :white }
115
- img.set_each_pixel_by_location do |x, y|
116
- :black if (x + y).odd?
151
+ img.set_each_pixel_by_location! do |x, y|
152
+ :red if (x + y).odd?
117
153
  end
118
154
 
119
- # count how many black pixels were set
120
- black = img.each_pixel.count { |c| c == :black }
155
+ # count how many red pixels were set
156
+ black = img.each_pixel.count { |c| c == :red }
157
+
158
+ # display img in terminal
159
+ img
121
160
  ```
122
161
 
162
+ ![Iterator example](docs/samples/iterator.png)
163
+
123
164
  Note that instead of manually calling `set_each_pixel_by_location`, you can just pass a block to `ImageUtil::Image.new`.
124
165
 
125
166
  ## Filters
@@ -132,7 +173,10 @@ modifies the image in place while the non-bang version returns a copy.
132
173
  Flatten an RGBA image on a solid color.
133
174
 
134
175
  ```ruby
176
+ # create a transparent image gradient containing shades of red only
135
177
  img = ImageUtil::Image.new(128, 128) { |x, y| [255, 0, 0, x + y] }
178
+
179
+ # put it on a blue background
136
180
  img.background([0, 0, 255])
137
181
  ```
138
182
 
@@ -169,23 +213,76 @@ img.draw_circle!([64, 64], 30, :blue)
169
213
  Scale an image to new dimensions.
170
214
 
171
215
  ```ruby
172
- img = ImageUtil::Image.new(256, 256) { |x, y| [x, y, 30] }
173
- img[70, 70] = img.resize(64, 64)
216
+ img = ImageUtil::Image.new(128, 128) { |x, y| [x, y, 30] }
217
+ img[20, 20] = img.resize(64, 64)
174
218
  img
175
219
  ```
176
220
 
177
221
  ![Resize example](docs/samples/resize.png)
178
222
 
179
- ### Dither
223
+ ### Palette
180
224
 
181
225
  Reduce the image to a limited palette.
182
226
 
183
227
  ```ruby
184
- img = ImageUtil::Image.new(256, 64) { |x, y| [x, y * 4, 200] }
185
- img.dither(8)
228
+ img = ImageUtil::Image.new(128, 128) { |x, y| [x * 2, y * 2, 200] }
229
+ img.palette_reduce(64)
186
230
  ```
187
231
 
188
- ![Dither example](docs/samples/dither.png)
232
+ ![Palette reduce example](docs/samples/pdither.png)
233
+
234
+ ### Transform
235
+
236
+ Rotate or flip an image.
237
+
238
+ ```ruby
239
+ img = ImageUtil::Image.new(128, 128) { |x, y| [x, y, 0] }
240
+ img.flip!(:x)
241
+ img.rotate!(90)
242
+ # img.rotate!(90, axes: [:x, :z])
243
+ ```
244
+
245
+ ![Transform example](docs/samples/transform.png)
246
+
247
+ ### Colors
248
+
249
+ Multiply all pixels by a color.
250
+
251
+ ```ruby
252
+ img = ImageUtil::Image.new(128, 128) { [255, 255, 255, 128] }
253
+ img * :red
254
+ ```
255
+
256
+ ![Colors example](docs/samples/colors.png)
257
+
258
+ ### Bitmap Text
259
+
260
+ Overlay text using the bundled bitmap font.
261
+
262
+ ```ruby
263
+ img = ImageUtil::Image.new(128, 128) { [0, 0, 0] }
264
+ img.bitmap_text!("Lorem ipsum dolor sit\namet, consectetur adipiscing\nelit.", 2, 2, color: :blue)
265
+ ```
266
+
267
+ ![Bitmap text example](docs/samples/bitmap_text.png)
268
+
269
+ ### Redimension
270
+
271
+ Change how many dimensions an image has or adjust their size.
272
+
273
+ ```ruby
274
+ img = ImageUtil::Image.new(64, 64) { :white }
275
+ img.redimension!(128, 128) # can also add additional dimensions
276
+ img.background(:blue)
277
+ ```
278
+
279
+ ![Redimension example](docs/samples/redimension.png)
280
+
281
+ ## Command Line
282
+
283
+ The gem includes a small `image_util` CLI. Run `image_util support` to list
284
+ available codecs, default format handlers and detected terminal features.
285
+ See [docs/cli.md](docs/cli.md) for details.
189
286
 
190
287
  ## Development
191
288
 
@@ -193,6 +290,10 @@ After checking out the repo, run `bin/setup` to install dependencies. Then run
193
290
  `rake spec` to execute the tests. You can also run `bin/console` for an
194
291
  interactive prompt for experimenting with the library.
195
292
 
293
+ ### Benchmarking
294
+
295
+ Run `bin/benchmark` to execute a small benchmark.
296
+
196
297
  ## Contributing
197
298
 
198
299
  Bug reports and pull requests are welcome on GitHub at
data/Rakefile CHANGED
@@ -8,3 +8,8 @@ RSpec::Core::RakeTask.new(:spec)
8
8
  RuboCop::RakeTask.new
9
9
 
10
10
  task default: %i[spec rubocop]
11
+
12
+ desc "Run benchmarks"
13
+ task :bench do
14
+ ruby File.expand_path("bin/benchmark", __dir__)
15
+ end
data/docs/cli.md ADDED
@@ -0,0 +1,5 @@
1
+ # Command Line Interface
2
+
3
+ Run `image_util support` to see which codecs are available on your system,
4
+ which codec handles each format by default, and which terminal features are
5
+ detected.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
data/exe/image_util ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "image_util"
6
+
7
+ ImageUtil::CLI.start(ARGV)
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "benchmark/ips"
4
+
5
+ module ImageUtil
6
+ module Benchmarking
7
+ module_function
8
+
9
+ # Benchmarks creating a 64×64×64 black image for the given time in seconds.
10
+ def image_creation(seconds = 5)
11
+ ::Benchmark.ips do |x|
12
+ x.warmup = 0
13
+ x.time = seconds
14
+ x.report("from Symbol") { Image.new(64, 64, 64) { :black } }
15
+ x.report("from Array (4->4 channels)") { Image.new(64, 64, 64) { |x,y,z| [x,y,z,255] } }
16
+ x.report("from Array (3->4 channels)") { Image.new(64, 64, 64) { |x,y,z| [x,y,z] } }
17
+ x.report("from Array (3->3 channels)") { Image.new(64, 64, 64, channels: 3) { |x,y,z| [x,y,z] } }
18
+ end
19
+ end
20
+
21
+ def run(seconds = 5)
22
+ image_creation(seconds)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1 @@
1
+ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`~!@#$%^&*()-_+=:;'"|\/?.,[]{}<>€
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ImageUtil
4
+ class BitmapFont
5
+ def initialize(name)
6
+ font = Image.from_file("#{__dir__}/bitmap_font/fonts/#{name}/font.png")
7
+ charset = File.read("#{__dir__}/bitmap_font/fonts/#{name}/charset.txt").chomp.chars
8
+
9
+ parse_image(font, charset)
10
+ end
11
+
12
+ def parse_image(font, charset)
13
+ font.height.times do |n|
14
+ if font[0,n] == :red
15
+ @height = n
16
+ break
17
+ end
18
+ end
19
+
20
+ character_pos = []
21
+
22
+ n = 0
23
+ while n < font.width
24
+ if font[n, @height] == :blue
25
+ start = n
26
+ n += 1 while n < font.width && font[n, @height] == :blue
27
+ n -= 1
28
+ finish = n
29
+ character_pos << (start..finish)
30
+ end
31
+ n += 1
32
+ end
33
+
34
+ font = font.dup
35
+ font.set_each_pixel_by_location! do |x,y|
36
+ [255, 255, 255, 255 - font[x,y].g]
37
+ end
38
+
39
+ @characters = character_pos.map.with_index do |range,idx|
40
+ [charset[idx], font[range, ...@height]]
41
+ end.to_h
42
+ end
43
+
44
+ def render_line_of_text(text)
45
+ width = 1
46
+ text.chars.each do |char|
47
+ width += @characters[char].width + 1
48
+ end
49
+ width -= 1
50
+
51
+ img = Image.new(width, @height)
52
+ width = 0
53
+ text.chars.each do |char|
54
+ img[width,0] = @characters[char]
55
+ width += @characters[char].width + 1
56
+ end
57
+
58
+ img
59
+ end
60
+
61
+ def self.fonts
62
+ Dir["#{__dir__}/bitmap_font/fonts/*"].map { |i| File.basename(i) }
63
+ end
64
+
65
+ def self.default_font = "smfont"
66
+
67
+ def self.cached_load(font)
68
+ @fonts ||= {}
69
+ @fonts[font] ||= BitmapFont.new(font)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module ImageUtil
6
+ class CLI < Thor
7
+ desc "support", "Display codec support, default codecs and terminal features"
8
+ def support
9
+ width = (codec_names + format_names).map(&:length).max
10
+ use_color = Terminal.detect_support.include?(:tty)
11
+
12
+ puts "Codecs:"
13
+ codec_names.each do |name|
14
+ mod = Codec.const_get(name)
15
+ supported = mod.supported?
16
+ status = supported ? color("supported", 32, use_color) : color("not supported", 31, use_color)
17
+ puts format(" %-#{width}s %s", name, status)
18
+ end
19
+
20
+ puts "\nFormats:"
21
+ format_names.each do |fmt|
22
+ codec = default_codec(fmt)
23
+ codec_name = codec ? color(codec.to_s, 32, use_color) : color("none", 31, use_color)
24
+ puts format(" %-#{width}s %s", fmt, codec_name)
25
+ end
26
+
27
+ puts "\nTerminal features:"
28
+ Terminal.detect_support.each do |feat|
29
+ puts " #{color(feat, 34, use_color)}"
30
+ end
31
+ end
32
+
33
+ no_commands do
34
+ def codec_names = Codec.constants.select { |name| Codec.const_get(name).respond_to?(:supported?) }
35
+ def format_names = (Codec.encoders + Codec.decoders).flat_map { |r| r[:formats] }.uniq.sort
36
+
37
+ def default_codec(fmt)
38
+ Codec.encoders.each do |r|
39
+ next unless r[:formats].include?(fmt.to_s)
40
+
41
+ codec_mod = Codec.const_get(r[:codec])
42
+ next if codec_mod.respond_to?(:supported?) && !codec_mod.supported?(fmt.to_sym)
43
+
44
+ return r[:codec]
45
+ end
46
+ nil
47
+ end
48
+
49
+ def color(text, code, enable)
50
+ enable ? "\e[#{code}m#{text}\e[0m" : text
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ImageUtil
4
+ module Codec
5
+ module ChunkyPng
6
+ SUPPORTED_FORMATS = [:png].freeze
7
+
8
+ extend Guard
9
+
10
+ begin
11
+ require "chunky_png"
12
+ AVAILABLE = true
13
+ rescue LoadError
14
+ AVAILABLE = false
15
+ end
16
+
17
+ module_function
18
+
19
+ def supported?(format = nil)
20
+ return false unless AVAILABLE
21
+
22
+ return true if format.nil?
23
+
24
+ SUPPORTED_FORMATS.include?(format.to_s.downcase.to_sym)
25
+ end
26
+
27
+ def encode(format, image)
28
+ guard_supported_format!(format, SUPPORTED_FORMATS)
29
+ raise UnsupportedFormatError, "chunky_png not available" unless AVAILABLE
30
+
31
+ guard_2d_image!(image)
32
+ guard_8bit_colors!(image)
33
+
34
+ raw = if image.channels == 4
35
+ image.buffer.get_string
36
+ else
37
+ data = String.new(capacity: image.width * image.height * 4)
38
+ buf = image.buffer
39
+ idx = 0
40
+ step = buf.pixel_bytes
41
+ image.height.times do
42
+ image.width.times do
43
+ color = buf.get_index(idx)
44
+ data << color[0].chr << color[1].chr << color[2].chr << 255.chr
45
+ idx += step
46
+ end
47
+ end
48
+ data
49
+ end
50
+
51
+ png = ::ChunkyPNG::Image.from_rgba_stream(image.width, image.height, raw)
52
+ png.to_blob
53
+ end
54
+
55
+ def decode(format, data)
56
+ guard_supported_format!(format, SUPPORTED_FORMATS)
57
+ raise UnsupportedFormatError, "chunky_png not available" unless AVAILABLE
58
+
59
+ png = ::ChunkyPNG::Image.from_blob(data)
60
+ raw = png.to_rgba_stream
61
+ io_buf = IO::Buffer.for(raw)
62
+ buf = Image::Buffer.new([png.width, png.height], 8, 4, io_buf)
63
+ Image.from_buffer(buf)
64
+ end
65
+ end
66
+ end
67
+ end