image_util 0.2.0 → 0.4.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 +36 -0
  4. data/README.md +133 -30
  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 +68 -15
  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 +63 -10
  36. data/lib/image_util/extension.rb +24 -0
  37. data/lib/image_util/filter/_mixin.rb +18 -0
  38. data/lib/image_util/filter/background.rb +7 -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 +4 -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 +76 -5
  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: 893e648382567326c70c82a4b3e3b6580889229c9d56c7a20ec4d182b45332b4
4
+ data.tar.gz: 70fd8a63b0551a0aaca12dae391c076fb01621443ec1ae5e5f92c680655277f2
5
5
  SHA512:
6
- metadata.gz: 523748193e1b51083729b1583cfec3ac19ec02a11e893cc3d858deb30388eebd013a01ade2770cd236c2f947bffc0cc2583783d3094552451d99b873d840311b
7
- data.tar.gz: 228f82c3508d236822c4a6e27ad721381ffa42aaa31d1e3ab839eeaf9d60363ab2bcd7e6340fab00cdeac23320ad4681036187bd2eb5bb5680242ff9a8451464
6
+ metadata.gz: 89a0a3bf90307337674acae7e1315505bc4a8363e58c041388dc6e6beeaab3f03e4534d2ff41e9a97c23f5775e62db6ebe5493658ac2b81e06f3a650a197d28a
7
+ data.tar.gz: 6b425b6539c389431e78cdc98744a82888a3f55e0fdeed64062bf69a3e0448fec84e83264b3dd1380133a5adafa87e5751dd36595c7c3dc413e4ead3173d21ae
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,39 @@
1
+ ## [0.4.0] - 2025-10-04
2
+ - BREAKING: Float color components now share the 0..255 range with integers instead of being scaled from 0..1.
3
+ - Add `define_mutable_version` helper to create bang versions from immutable methods
4
+ - Add `background!` and `resize!` filters
5
+
6
+ ## [0.3.0] - 2025-07-25
7
+ - Rename `dither!` to `palette_reduce!`
8
+ - Rename `#set_each_pixel_by_location` to `#set_each_pixel_by_location!` since it's mutable
9
+ - Rename `color_length` to a more appropriate name `channels`
10
+ - Thor based CLI with a `support` command that lists codec support, default
11
+ format handlers and detected terminal features
12
+ - Replace `palette_reduce!` implementation with a much faster one
13
+ - Terminal detection for graphic protocols
14
+ - Support Kitty graphics protocol
15
+ - Transform filter with rotate and flip operations
16
+ - Fallback PNG codec via chunky_png
17
+ - ImageMagick codec can now read and write `gif` and `apng` including animations
18
+ - Redimension filter to change image dimensions
19
+ - Sixel and Kitty codecs accept 1D images
20
+ - `Pam.encode` no longer accepts `fill_to`; Sixel codecs pad images using the
21
+ redimension filter
22
+ - Add `BitmapFont` with a sample hand crafted font, add `bitmap_text` generator.
23
+ - `Color#*` can now accept another `Color` to multiply channels
24
+ - Add `Colors` filter with `color_multiply!` and alias `*`
25
+ - `bitmap_text` accepts multiline strings and supports colorization
26
+ - `bitmap_text` supports left, center and right alignment
27
+ - Add `BitmapText` filter for overlaying text onto images
28
+ - `bitmap_text` filter now always respects an alpha channel when pasting text
29
+ - Open ImageMagick codec pipes in binary mode for Windows compatibility
30
+ - Format inference from file extension in `Image#to_file`
31
+ - ImageMagick codec now reads PAM frames using the Pam codec
32
+ - Force 8-bit output when decoding through ImageMagick to avoid 1-bit images on Windows
33
+ - ImageMagick codec checks available formats before advertising APNG support
34
+ - Add `Example` generator: `Image.example_rose`
35
+ - Disable APNG on Windows until we find out something else than ImageMagick to support it
36
+
1
37
  ## [0.2.0] - 2025-07-21
2
38
  - Ruby Sixel encoder now sets pixel aspect ratio metadata to display correctly in Windows Terminal
3
39
  - 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
 
@@ -68,13 +96,13 @@ This library supports generating Sixel with either `libsixel`, `ImageMagick` or
68
96
  - Symbols or strings containing CSS color names (`:rebeccapurple`, 'papayawhip')
69
97
  - Hex strings like `'#abc'`, `'#aabbcc'` or `'#rrggbbaa'`
70
98
 
71
- When numeric components are given, integers are first clamped to the `0..255`
72
- range. Float values are treated as fractions of 255, so `0.5` becomes `127.5`
73
- and `1.0` becomes `255`. After scaling, values are again clamped to this range.
74
- If the alpha channel is omitted it defaults to `255`.
99
+ When numeric components are given, values are clamped directly to the `0..255`
100
+ range regardless of whether they are integers or floats. Floating point inputs
101
+ are no longer scaled from `0..1`; instead they are treated in the same units as
102
+ integers. If the alpha channel is omitted it defaults to `255`.
75
103
 
76
104
  ```ruby
77
- ImageUtil::Color[0.5] # => #808080
105
+ ImageUtil::Color[128.5] # => #808080
78
106
  ImageUtil::Color[:red] # => #ff0000
79
107
  ImageUtil::Color["#fc0"] # => #ffcc00
80
108
  ```
@@ -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,8 +173,12 @@ 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] }
136
- img.background([0, 0, 255])
178
+
179
+ # put it on a blue background
180
+ img.background!([0, 0, 255])
181
+ # img.background([0, 0, 255]) # returns a new image
137
182
  ```
138
183
 
139
184
  ![Background example](docs/samples/background.png)
@@ -169,23 +214,77 @@ img.draw_circle!([64, 64], 30, :blue)
169
214
  Scale an image to new dimensions.
170
215
 
171
216
  ```ruby
172
- img = ImageUtil::Image.new(256, 256) { |x, y| [x, y, 30] }
173
- img[70, 70] = img.resize(64, 64)
217
+ img = ImageUtil::Image.new(128, 128) { |x, y| [x, y, 30] }
218
+ img[20, 20] = img.resize(64, 64)
174
219
  img
220
+ # img.resize!(64, 64) # modifies in place
175
221
  ```
176
222
 
177
223
  ![Resize example](docs/samples/resize.png)
178
224
 
179
- ### Dither
225
+ ### Palette
180
226
 
181
227
  Reduce the image to a limited palette.
182
228
 
183
229
  ```ruby
184
- img = ImageUtil::Image.new(256, 64) { |x, y| [x, y * 4, 200] }
185
- img.dither(8)
230
+ img = ImageUtil::Image.new(128, 128) { |x, y| [x * 2, y * 2, 200] }
231
+ img.palette_reduce(64)
186
232
  ```
187
233
 
188
- ![Dither example](docs/samples/dither.png)
234
+ ![Palette reduce example](docs/samples/pdither.png)
235
+
236
+ ### Transform
237
+
238
+ Rotate or flip an image.
239
+
240
+ ```ruby
241
+ img = ImageUtil::Image.new(128, 128) { |x, y| [x, y, 0] }
242
+ img.flip!(:x)
243
+ img.rotate!(90)
244
+ # img.rotate!(90, axes: [:x, :z])
245
+ ```
246
+
247
+ ![Transform example](docs/samples/transform.png)
248
+
249
+ ### Colors
250
+
251
+ Multiply all pixels by a color.
252
+
253
+ ```ruby
254
+ img = ImageUtil::Image.new(128, 128) { [255, 255, 255, 128] }
255
+ img * :red
256
+ ```
257
+
258
+ ![Colors example](docs/samples/colors.png)
259
+
260
+ ### Bitmap Text
261
+
262
+ Overlay text using the bundled bitmap font.
263
+
264
+ ```ruby
265
+ img = ImageUtil::Image.new(128, 128) { [0, 0, 0] }
266
+ img.bitmap_text!("Lorem ipsum dolor sit\namet, consectetur adipiscing\nelit.", 2, 2, color: :blue)
267
+ ```
268
+
269
+ ![Bitmap text example](docs/samples/bitmap_text.png)
270
+
271
+ ### Redimension
272
+
273
+ Change how many dimensions an image has or adjust their size.
274
+
275
+ ```ruby
276
+ img = ImageUtil::Image.new(64, 64) { :white }
277
+ img.redimension!(128, 128) # can also add additional dimensions
278
+ img.background(:blue)
279
+ ```
280
+
281
+ ![Redimension example](docs/samples/redimension.png)
282
+
283
+ ## Command Line
284
+
285
+ The gem includes a small `image_util` CLI. Run `image_util support` to list
286
+ available codecs, default format handlers and detected terminal features.
287
+ See [docs/cli.md](docs/cli.md) for details.
189
288
 
190
289
  ## Development
191
290
 
@@ -193,6 +292,10 @@ After checking out the repo, run `bin/setup` to install dependencies. Then run
193
292
  `rake spec` to execute the tests. You can also run `bin/console` for an
194
293
  interactive prompt for experimenting with the library.
195
294
 
295
+ ### Benchmarking
296
+
297
+ Run `bin/benchmark` to execute a small benchmark.
298
+
196
299
  ## Contributing
197
300
 
198
301
  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