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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 27dbed58dd9646ef50f6ae022f3d2599629670f0d430f61c76416cb28d49bbd6
4
- data.tar.gz: 55d78376ffbbf3f85b60db158ca65c46b96164c9e6c52b0d7324b70e1bd11dc4
3
+ metadata.gz: 31687d0660e040e340b6ed26f581f95de0746e33f992acf7dfcdb4fa4a88f4cd
4
+ data.tar.gz: 4fee3793295fb2207a54caa1ed430bdc43ddc8307f96ef597ba0ed777618b8e6
5
5
  SHA512:
6
- metadata.gz: 2c14b302a030e83cc6f398f8560138c31f53e93d9059c7f2b18ce5b81aeb41e4922932a64e8221b5e2e52fc4e5c876ac4dfd74f0eb7630e907e5a74a50a52335
7
- data.tar.gz: 6f3834f813239cc80418d54a01224ae5afbd28870a8beff19f1a2c32fabfe7a800d2cc59a857d98faed713c49fff2cc0519332927004c900a72709b08162bcc3
6
+ metadata.gz: c02064cb968adae5e5d5b5328922e77fd542d6ca4322a9259d27b513339ff44ca71e272f8cf01055d864f3dbc3fef702d3187a745e98af09a95f2dc9907470c4
7
+ data.tar.gz: 97c9852b31aab0d2b71f19579968d856fd87eed7339c3b2ec9e6bd1dcee3b674862821f6d4a033148db8d45b0a8228d890670d43c2f0ba921dc98d7d5ed774de
data/AGENTS.md CHANGED
@@ -6,11 +6,10 @@
6
6
  - Prefer double-quoted strings except in specs and the gemspec.
7
7
  - Use RSpec's `should` syntax instead of `expect`.
8
8
  - For one-line methods, use the `def name = expression` style.
9
-
10
- Additional notes from the existing code:
11
- - Image data is stored in `Image::Buffer` backed by `IO::Buffer`.
12
- - Use `Filter::Mixin#define_immutable_version` to add non-bang versions of mutating filters.
13
- - Views such as `View::Interpolated` and `View::Rounded` are built with `Data.define`.
14
- - Pure Ruby algorithms are provided with optional FFI wrappers for libpng, libturbojpeg and libsixel.
9
+ - After adding new features or modifying existing ones, change documentation accordingly (especially README and especially CHANGELOG).
10
+ - Don't discuss codec internals or bug fixes in README. Only list supported formats. Document bug fixes in the CHANGELOG, not in README.
15
11
  - Specs target at least 80% coverage as enforced by SimpleCov.
16
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,5 +1,44 @@
1
- ## [Unreleased]
2
- ### Added
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
+
32
+ ## [0.2.0] - 2025-07-21
33
+ - Ruby Sixel encoder now sets pixel aspect ratio metadata to display correctly in Windows Terminal
34
+ - Support for all CSS color names
35
+ - Range assignments with images now resize the image before pasting
36
+ - Circle drawing filter
37
+ - ImageMagick codec can encode and decode `png`, `jpeg` and `sixel`
38
+
39
+ ## [0.1.0] - 2025-07-21
40
+
41
+ - Initial release
3
42
  - Drop support for Ruby 3.1
4
43
  - Native SIXEL encoder
5
44
  - Background filter
@@ -12,7 +51,3 @@
12
51
  - Paste and Draw filters for compositing and drawing
13
52
  - Rounded and interpolated pixel views
14
53
  - `Image#view` helper for custom access
15
-
16
- ## [0.1.0] - 2025-07-19
17
-
18
- - Initial release
data/README.md CHANGED
@@ -1,144 +1,288 @@
1
1
  # ImageUtil
2
2
 
3
- ImageUtil provides a minimal in-memory image container and a set of utilities for handling raw pixel data in Ruby. It is aimed at small scripts and tools that need to manipulate images without relying on heavy external dependencies.
3
+ ImageUtil is a lightweight Ruby library focused on manipulating images directly in memory. Its primary goal is to help scripts visualize data right in the terminal by supporting SIXEL output alongside common image formats. The API is still evolving and should be considered unstable until version 1.0.
4
4
 
5
- Features include:
5
+ ## Creating an Image
6
6
 
7
- * Representation of images with arbitrary dimensions.
8
- * Support for 8, 16 or 32 bit components and RGB or RGBA color values.
9
- * A `Color` helper class capable of parsing numbers, arrays and HTML style strings.
10
- * Conversion of an image to PAM or SIXEL for quick previews in compatible terminals.
11
- * Built-in SIXEL encoder that works without ImageMagick.
12
- * Convenience methods for iterating over pixel locations and setting values.
13
- * Overlaying colors with the `+` operator which blends using the alpha channel.
14
- * Automatic format detection when reading images from strings or files.
15
- * Alternate pixel views for interpolated or rounded coordinates.
7
+ ```ruby
8
+ require 'image_util'
9
+
10
+ # 40×40 RGBA image
11
+ img = ImageUtil::Image.new(40, 40)
12
+ ```
13
+
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
+
16
+ ```ruby
17
+ img = ImageUtil::Image.new(128, 128) { |x, y| ImageUtil::Color[x, y, 40] }
18
+ ```
19
+
20
+ ![Constructor example](docs/samples/constructor.png)
21
+
22
+ ## Loading and Saving
16
23
 
17
- ## Installation
24
+ Instead of building an image from scratch you can load one with
25
+ `ImageUtil::Image.from_string` or `ImageUtil::Image.from_file`.
26
+ Both helpers understand the built in codecs for `png`, `jpeg`, `pam`, `gif`
27
+ and `apng` formats:
18
28
 
19
- ImageUtil is available on RubyGems:
29
+ ```ruby
30
+ img = ImageUtil::Image.from_file("logo.png")
31
+ data = ImageUtil::Image.from_string(File.binread("logo.jpeg"))
32
+ ```
33
+
34
+ A `from_file` method also supports passing IO objects:
20
35
 
21
- ```bash
22
- gem install image_util
36
+ ```ruby
37
+ img = ImageUtil::Image.from_file(IO.popen("magick rose: png:"))
38
+ img.draw_line([0,0], [69,45], :blue)
23
39
  ```
24
40
 
25
- Alternatively add it to your `Gemfile`:
41
+ ![Pipe load example](docs/samples/pipe.png)
42
+
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.
26
45
 
27
46
  ```ruby
28
- gem "image_util"
47
+ img.to_file("out.png")
48
+ binary = img.to_string(:jpeg)
29
49
  ```
30
50
 
31
- Run `bundle install` afterwards.
51
+ ## Image Information
32
52
 
33
- You can also build and install the gem manually:
53
+ After loading or creating an image, you can access information about it, like
54
+ dimensions or color bits.
34
55
 
35
- ```bash
36
- git clone https://github.com/rbutils/image_util.git
37
- cd image_util
38
- bundle exec rake install
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)
39
62
  ```
40
63
 
41
- ## Usage
64
+ ## Terminal Output
65
+
66
+ Images can be previewed in compatible terminals:
42
67
 
43
68
  ```ruby
44
- require 'image_util'
69
+ puts ImageUtil::Terminal.output_image($stdin, $stdout, img)
70
+ ```
71
+
72
+ In `irb` or `pry` the `inspect` method shows the image automatically, so you can
73
+ just evaluate the object:
45
74
 
46
- # create a 4×4 image using 8‑bit RGBA colors
47
- i = ImageUtil::Image.new(4, 4)
75
+ ```ruby
76
+ img
77
+ ```
78
+
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:
82
+
83
+ ![Sixel example](docs/samples/sixel.png)
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`.
87
+
88
+
89
+ ## Color Values
90
+
91
+ `ImageUtil::Color.from` (also known as `ImageUtil::Color.[]`) accepts several inputs:
92
+
93
+ - Another `Color` instance
94
+ - Arrays of numeric components (`[r, g, b]` or `[r, g, b, a]`)
95
+ - Numbers (used for all RGB channels)
96
+ - Symbols or strings containing CSS color names (`:rebeccapurple`, 'papayawhip')
97
+ - Hex strings like `'#abc'`, `'#aabbcc'` or `'#rrggbbaa'`
98
+
99
+ When numeric components are given, integers are first clamped to the `0..255`
100
+ range. Float values are treated as fractions of 255, so `0.5` becomes `127.5`
101
+ and `1.0` becomes `255`. After scaling, values are again clamped to this range.
102
+ If the alpha channel is omitted it defaults to `255`.
103
+
104
+ ```ruby
105
+ ImageUtil::Color[0.5] # => #808080
106
+ ImageUtil::Color[:red] # => #ff0000
107
+ ImageUtil::Color["#fc0"] # => #ffcc00
108
+ ```
109
+
110
+ Note that whenever the library expects a color, it may be given in any form accepted by this function.
111
+
112
+ ## Pixel Access
113
+
114
+ Pixels can be accessed with integer coordinates or ranges. When ranges are used
115
+ a new `Image` containing that region is returned and can be modified separately.
116
+
117
+ ```ruby
118
+ img[0, 0] = '#ff0000'
119
+ patch = img[0..1, 0..1]
120
+ ```
48
121
 
49
- # set the top‑left pixel to red
50
- i[0, 0] = ImageUtil::Color[255, 0, 0]
122
+ For instance, you can extract a region, edit it and paste it back:
51
123
 
52
- # display the image in a SIXEL-capable terminal
53
- puts i.to_sixel
124
+ ```ruby
125
+ img = ImageUtil::Image.new(128, 128) { [0, 0, 0] }
126
+ corner = img[0..32, 0..32]
127
+ corner.all = :green
128
+ img[0..32, 0..32] = corner
129
+ img[2, 2] = :yellow
130
+ # img.to_file("pixel_patch.png", :png)
131
+ img
54
132
  ```
55
133
 
56
- Images can also be iterated over or modified using ranges:
134
+ ![Range access example](docs/samples/range.png)
135
+
136
+ Assigning an image to a range automatically resizes it to fit before pasting.
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
+
141
+ Iteration helpers operate on arbitrary ranges and share the same syntax used
142
+ when indexing images. `each_pixel` yields color objects, while
143
+ `each_pixel_location` yields coordinate arrays. `set_each_pixel_by_location!`
144
+ assigns the value returned by the block to every location (unless `nil` is returned).
57
145
 
58
146
  ```ruby
59
- # fill an area with blue
60
- i[0..1, 0..1] = ImageUtil::Color['#0000ff']
147
+ # create an all-black image
148
+ img = ImageUtil::Image.new(128, 128) { :black }
61
149
 
62
- # iterate over every pixel
63
- i.each_pixel do |pixel|
64
- # pixel is an ImageUtil::Color instance
150
+ # fill a checkerboard pattern
151
+ img.set_each_pixel_by_location! do |x, y|
152
+ :red if (x + y).odd?
65
153
  end
66
154
 
67
- # paste one image into another
68
- target = ImageUtil::Image.new(8, 8) { ImageUtil::Color[0] }
69
- source = ImageUtil::Image.new(2, 2) { ImageUtil::Color[255, 0, 0, 128] }
70
- target.paste!(source, 3, 3, respect_alpha: true)
155
+ # count how many red pixels were set
156
+ black = img.each_pixel.count { |c| c == :red }
71
157
 
72
- # draw a diagonal line
73
- i.draw_line!([0, 0], [3, 3], ImageUtil::Color['red'], view: ImageUtil::View::Rounded)
158
+ # display img in terminal
159
+ img
74
160
  ```
75
161
 
76
- `View::Interpolated` provides subpixel access while `View::Rounded` snaps
77
- coordinates to the nearest pixel. These views are useful for drawing
78
- operations like the example above.
162
+ ![Iterator example](docs/samples/iterator.png)
163
+
164
+ Note that instead of manually calling `set_each_pixel_by_location`, you can just pass a block to `ImageUtil::Image.new`.
79
165
 
80
- ### Reading and Writing Images
166
+ ## Filters
167
+
168
+ `ImageUtil::Image` ships with a few convenience filters. Each bang method
169
+ modifies the image in place while the non-bang version returns a copy.
170
+
171
+ ### Background
172
+
173
+ Flatten an RGBA image on a solid color.
81
174
 
82
175
  ```ruby
83
- # detect format automatically when loading from a file
84
- img = ImageUtil::Image.from_file("photo.png")
176
+ # create a transparent image gradient containing shades of red only
177
+ img = ImageUtil::Image.new(128, 128) { |x, y| [255, 0, 0, x + y] }
178
+
179
+ # put it on a blue background
180
+ img.background([0, 0, 255])
181
+ ```
85
182
 
86
- # save using a specific codec
87
- img.to_file("out.jpg", :jpeg)
183
+ ![Background example](docs/samples/background.png)
88
184
 
89
- # convert directly to a string
90
- data = img.to_string(:png)
185
+ ### Paste
186
+
187
+ Place one image over another. When `respect_alpha` is true, the pasted pixels are
188
+ blended with the base image.
189
+
190
+ ```ruby
191
+ base = ImageUtil::Image.new(128, 128) { |x, y| [x, y, 50] }
192
+ overlay = ImageUtil::Image.new(64, 64) { |x, y| [255, 0, 0, (x + y) * 2] }
193
+ base.paste!(overlay, 32, 32, respect_alpha: true)
91
194
  ```
92
195
 
93
- ### Filters
196
+ ![Paste example](docs/samples/paste.png)
197
+
198
+ ### Draw
199
+
200
+ Draw simple shapes directly on the image.
94
201
 
95
202
  ```ruby
96
- # reduce palette to 32 colors
97
- dithered = img.dither(32)
203
+ img = ImageUtil::Image.new(128, 128) { [0, 0, 0] }
204
+ img.draw_line!([0, 0], [127, 127], :red)
205
+ img.draw_line!([0, 127], [127, 0], :lime)
206
+ img.draw_circle!([64, 64], 30, :blue)
207
+ ```
208
+
209
+ ![Draw example](docs/samples/draw.png)
210
+
211
+ ### Resize
98
212
 
99
- # composite two images without altering the originals
100
- result = base.paste(other, 10, 10)
213
+ Scale an image to new dimensions.
101
214
 
102
- # apply a background color to an RGBA image
103
- flattened = img.background(ImageUtil::Color[255, 255, 255])
215
+ ```ruby
216
+ img = ImageUtil::Image.new(128, 128) { |x, y| [x, y, 30] }
217
+ img[20, 20] = img.resize(64, 64)
218
+ img
104
219
  ```
105
220
 
106
- ### Working with Views
221
+ ![Resize example](docs/samples/resize.png)
222
+
223
+ ### Palette
224
+
225
+ Reduce the image to a limited palette.
107
226
 
108
227
  ```ruby
109
- # access using fractional coordinates
110
- interp = img.view(ImageUtil::View::Interpolated)
111
- interp[1.2, 2.8] = ImageUtil::Color[0, 0, 255]
228
+ img = ImageUtil::Image.new(128, 128) { |x, y| [x * 2, y * 2, 200] }
229
+ img.palette_reduce(64)
230
+ ```
231
+
232
+ ![Palette reduce example](docs/samples/pdither.png)
233
+
234
+ ### Transform
235
+
236
+ Rotate or flip an image.
112
237
 
113
- # round coordinates instead
114
- rounded = img.view(ImageUtil::View::Rounded)
115
- color = rounded[1.6, 0.3]
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])
116
243
  ```
117
244
 
118
- ### Codecs
245
+ ![Transform example](docs/samples/transform.png)
246
+
247
+ ### Colors
119
248
 
120
- ImageUtil includes a small registry of codecs for converting images to and from
121
- common formats such as PNG, JPEG and SIXEL. The library ships with pure Ruby
122
- encoders and FFI wrappers around `libpng`, `libturbojpeg` and `libsixel` when
123
- available.
249
+ Multiply all pixels by a color.
124
250
 
125
251
  ```ruby
126
- png = ImageUtil::Codec.encode(:png, i)
127
- back = ImageUtil::Codec.decode(:png, png)
252
+ img = ImageUtil::Image.new(128, 128) { [255, 255, 255, 128] }
253
+ img * :red
254
+ ```
128
255
 
129
- File.open("img.pam", "wb") do |f|
130
- ImageUtil::Codec.encode_io(:pam, i, f)
131
- end
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)
132
265
  ```
133
266
 
134
- You can read images from files without specifying the format:
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.
135
272
 
136
273
  ```ruby
137
- image = ImageUtil::Image.from_file("picture.jpg")
274
+ img = ImageUtil::Image.new(64, 64) { :white }
275
+ img.redimension!(128, 128) # can also add additional dimensions
276
+ img.background(:blue)
138
277
  ```
139
278
 
140
- Use `ImageUtil::Codec.supported?(format)` to check if a particular format is
141
- available. Unsupported formats raise `ImageUtil::Codec::UnsupportedFormatError`.
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.
142
286
 
143
287
  ## Development
144
288
 
@@ -146,6 +290,10 @@ After checking out the repo, run `bin/setup` to install dependencies. Then run
146
290
  `rake spec` to execute the tests. You can also run `bin/console` for an
147
291
  interactive prompt for experimenting with the library.
148
292
 
293
+ ### Benchmarking
294
+
295
+ Run `bin/benchmark` to execute a small benchmark.
296
+
149
297
  ## Contributing
150
298
 
151
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
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