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
@@ -3,16 +3,15 @@
3
3
  module ImageUtil
4
4
  class Image
5
5
  autoload :Buffer, "image_util/image/buffer"
6
- autoload :PixelView, "image_util/image/pixel_view"
7
6
 
8
7
  Util.irb_fixup
9
8
 
10
9
  ALL = nil..nil
11
10
 
12
- def initialize(*dimensions, color_bits: 8, color_length: 4, &block)
13
- @buf = Buffer.new(dimensions, color_bits, color_length)
11
+ def initialize(*dimensions, color_bits: 8, channels: 4, &block)
12
+ @buf = Buffer.new(dimensions, color_bits, channels)
14
13
 
15
- set_each_pixel_by_location(&block) if block_given?
14
+ set_each_pixel_by_location!(&block) if block_given?
16
15
  end
17
16
 
18
17
  def initialize_from_buffer(buffer)
@@ -66,10 +65,10 @@ module ImageUtil
66
65
  def height = dimensions[1]
67
66
  def length = dimensions[2]
68
67
  def color_bits = @buf.color_bits
69
- def color_length = @buf.color_length
68
+ def channels = @buf.channels
70
69
  def pixel_bytes = @buf.pixel_bytes
71
70
 
72
- def location_expand(location)
71
+ def location_expand(location = full_image_location)
73
72
  counts = []
74
73
 
75
74
  location = location.reverse.map.with_index do |i,idx|
@@ -109,10 +108,10 @@ module ImageUtil
109
108
  new_dimensions, locations = location_expand(location)
110
109
  new_image = Image.new(*new_dimensions,
111
110
  color_bits: color_bits,
112
- color_length: color_length)
111
+ channels: channels)
113
112
 
114
113
  locations.each_with_index do |i, idx|
115
- new_image.buffer.set_index(idx * @buf.pixel_bytes, @buf.get(i))
114
+ new_image.buffer.set_index(idx * pixel_bytes, @buf.get(i))
116
115
  end
117
116
 
118
117
  new_image
@@ -169,28 +168,39 @@ module ImageUtil
169
168
  end
170
169
 
171
170
  include Enumerable
172
- include Filter::Dither
171
+ include Filter::Palette
173
172
  include Filter::Background
174
173
  include Filter::Paste
175
174
  include Filter::Draw
176
175
  include Filter::Resize
177
- include Statistic::Color
176
+ include Filter::Transform
177
+ include Filter::Redimension
178
+ include Filter::Colors
179
+ include Filter::BitmapText
180
+ include Statistic::Colors
181
+ extend Generator::BitmapText
182
+ extend Generator::Example
178
183
 
179
184
  def length = dimensions.last
180
185
 
181
- def to_pam(fill_to: nil)
182
- Codec.encode(:pam, self, fill_to: fill_to)
186
+ def to_pam
187
+ Codec.encode(:pam, self)
183
188
  end
184
189
 
185
190
  def to_string(format, codec: nil, **kwargs)
186
191
  Codec.encode(format, self, codec: codec, **kwargs)
187
192
  end
188
193
 
189
- def to_file(path_or_io, format, codec: nil, **kwargs)
194
+ def to_file(path_or_io, format = nil, codec: nil, **kwargs)
190
195
  if path_or_io.respond_to?(:write)
196
+ raise ArgumentError, "format required" unless format
197
+
191
198
  path_or_io.binmode if path_or_io.respond_to?(:binmode)
192
199
  Codec.encode_io(format, self, path_or_io, codec: codec, **kwargs)
193
200
  else
201
+ format ||= Extension.detect(path_or_io)
202
+ raise ArgumentError, "could not detect format" unless format
203
+
194
204
  File.open(path_or_io, "wb") do |io|
195
205
  Codec.encode_io(format, self, io, codec: codec, **kwargs)
196
206
  end
@@ -201,12 +211,14 @@ module ImageUtil
201
211
  Codec.encode(:sixel, self)
202
212
  end
203
213
 
204
- alias inspect to_sixel
205
-
206
214
  def pretty_print(p)
207
- p.flush
208
- p.output << to_sixel
209
- p.text("", 0)
215
+ if (image = Terminal.output_image($stdin, $stdout, self))
216
+ p.flush
217
+ p.output << image
218
+ p.text("", 0)
219
+ else
220
+ super
221
+ end
210
222
  end
211
223
 
212
224
  def pixel_count(locations) = location_expand(locations).first.reduce(:*)
@@ -223,12 +235,26 @@ module ImageUtil
223
235
  end
224
236
  end
225
237
 
226
- def set_each_pixel_by_location(locations = full_image_location)
227
- return enum_for(:set_each_pixel_by_location) { pixel_count(locations) } unless block_given?
238
+ def set_each_pixel_by_location!(locations = full_image_location)
239
+ return enum_for(:set_each_pixel_by_location!) { pixel_count(locations) } unless block_given?
228
240
 
229
- each_pixel_location(locations) do |location|
230
- value = yield location
231
- self[*location] = value if value
241
+ if locations == full_image_location
242
+ # Optimized path
243
+ pixels, locations = location_expand
244
+
245
+ iter = 0
246
+ pixels = pixels.reduce(:*)
247
+
248
+ while iter < pixels
249
+ value = yield locations[iter]
250
+ buffer.set_index(iter * pixel_bytes, value) if value
251
+ iter += 1
252
+ end
253
+ else
254
+ each_pixel_location(locations) do |location|
255
+ value = yield location
256
+ self[*location] = value if value
257
+ end
232
258
  end
233
259
  end
234
260
 
@@ -7,7 +7,8 @@ module ImageUtil
7
7
  MAGIC_NUMBERS = {
8
8
  pam: "P7\n".b,
9
9
  png: "\x89PNG\r\n\x1a\n".b,
10
- jpeg: "\xFF\xD8".b
10
+ jpeg: "\xFF\xD8".b,
11
+ gif: "GIF8".b
11
12
  }.freeze
12
13
 
13
14
  BYTES_NEEDED = MAGIC_NUMBERS.values.map(&:bytesize).max
@@ -19,16 +20,18 @@ module ImageUtil
19
20
  def detect(data)
20
21
  return nil unless data
21
22
 
23
+ if data.start_with?(MAGIC_NUMBERS[:png]) && data.byteslice(0, 256).include?("acTL")
24
+ return :apng
25
+ end
26
+
22
27
  MAGIC_NUMBERS.each do |fmt, magic|
23
28
  return fmt if data.start_with?(magic)
24
- crlf_magic = magic.gsub("\n", "\r\n")
25
- return fmt if crlf_magic != magic && data.start_with?(crlf_magic)
26
29
  end
30
+
27
31
  nil
28
32
  end
29
33
 
30
- def detect_io(io)
31
-
34
+ def detect_io(io)
32
35
  pos = io.pos
33
36
  data = io.read(BYTES_NEEDED)
34
37
  io.seek(pos)
@@ -39,7 +42,6 @@ module ImageUtil
39
42
  rest = io.read
40
43
  new_io = StringIO.new((data || "") + (rest || ""))
41
44
  [fmt, new_io]
42
-
43
45
  end
44
46
  end
45
47
  end
@@ -1,8 +1,8 @@
1
1
  module ImageUtil
2
2
  module Statistic
3
- module Color
3
+ module Colors
4
4
  def histogram = each_pixel.to_a.tally
5
- def unique_colors = histogram.keys
5
+ def unique_colors = each_pixel.to_a.uniq
6
6
  def unique_color_count = unique_colors.length
7
7
  end
8
8
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ImageUtil
4
4
  module Statistic
5
- autoload :Color, "image_util/statistic/color"
5
+ autoload :Colors, "image_util/statistic/colors"
6
6
  end
7
7
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "io/console"
4
+
5
+ module ImageUtil
6
+ module Terminal
7
+ module_function
8
+
9
+ def detect_support(termin = $stdin, termout = $stdout)
10
+ return [] if !termin.tty? || !termout.tty?
11
+
12
+ supported = termout.instance_variable_get(:@imageutil_support_cache)
13
+ return supported if supported
14
+
15
+ supported = [:tty]
16
+
17
+ # Send kitty query
18
+ query_terminal(termin, termout, "\e_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\e\\\e[c".b) do |resp|
19
+ resp.start_with?("\e_G".b) && resp.include?("OK".b)
20
+ end and supported << :kitty
21
+
22
+ # Send sixel query
23
+ query_terminal(termin, termout, "\e[0c".b) do |resp|
24
+ resp.include?(";4".b)
25
+ end and supported << :sixel
26
+
27
+ termout.instance_variable_set(:@imageutil_support_cache, supported)
28
+ end
29
+
30
+ def query_terminal(termin, termout, query, timeout = 0.2)
31
+ resp = ""
32
+ termin.raw do
33
+ termout.write query
34
+ termout.flush
35
+ t0 = Time.now
36
+ loop do
37
+ begin
38
+ resp += termin.read_nonblock(512)
39
+ break if resp.start_with?("\e".b)
40
+ rescue IO::WaitReadable
41
+ IO.select([termin], nil, nil, timeout)
42
+ end
43
+ break if Time.now - t0 > timeout
44
+ end
45
+ end
46
+ yield resp
47
+ rescue EOFError, Errno::EBADF
48
+ false
49
+ end
50
+
51
+ def output_image(termin, termout, image)
52
+ support = detect_support(termin, termout)
53
+
54
+ if support.include? :kitty
55
+ image.to_string(:kitty)
56
+ elsif support.include? :sixel
57
+ image.to_string(:sixel)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ImageUtil
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -24,7 +24,7 @@ module ImageUtil
24
24
  end
25
25
 
26
26
  def [](*location)
27
- accum = Array.new(image.color_length, 0.0)
27
+ accum = Array.new(image.channels, 0.0)
28
28
  generate_subpixel_hash(location).each do |loc, weight|
29
29
  image[*loc].each_with_index do |val, idx|
30
30
  accum[idx] += val * weight
data/lib/image_util.rb CHANGED
@@ -6,12 +6,18 @@ module ImageUtil
6
6
  class Error < StandardError; end
7
7
  # Your code goes here...
8
8
 
9
+ autoload :BitmapFont, "image_util/bitmap_font"
9
10
  autoload :Color, "image_util/color"
10
11
  autoload :Image, "image_util/image"
11
12
  autoload :Util, "image_util/util"
12
13
  autoload :Codec, "image_util/codec"
13
14
  autoload :Magic, "image_util/magic"
15
+ autoload :Extension, "image_util/extension"
14
16
  autoload :Filter, "image_util/filter"
17
+ autoload :Generator, "image_util/generator"
15
18
  autoload :Statistic, "image_util/statistic"
19
+ autoload :Terminal, "image_util/terminal"
16
20
  autoload :View, "image_util/view"
21
+ autoload :CLI, "image_util/cli"
22
+ autoload :Benchmarking, "image_util/benchmarking"
17
23
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: image_util
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - hmdne
@@ -9,6 +9,20 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: ffi
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -23,11 +37,40 @@ dependencies:
23
37
  - - "~>"
24
38
  - !ruby/object:Gem::Version
25
39
  version: '1.16'
40
+ - !ruby/object:Gem::Dependency
41
+ name: io-console
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.5'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.5'
54
+ - !ruby/object:Gem::Dependency
55
+ name: thor
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.2'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.2'
26
68
  description: Lightweight Color and Image classes for manipulating pixels. Provides
27
69
  SIXEL output plus FFI bindings for libpng, libturbojpeg and libsixel.
28
70
  email:
29
71
  - 54514036+hmdne@users.noreply.github.com
30
- executables: []
72
+ executables:
73
+ - image_util
31
74
  extensions: []
32
75
  extra_rdoc_files: []
33
76
  files:
@@ -38,16 +81,34 @@ files:
38
81
  - LICENSE.txt
39
82
  - README.md
40
83
  - Rakefile
84
+ - docs/cli.md
41
85
  - docs/samples/background.png
86
+ - docs/samples/bitmap_text.png
87
+ - docs/samples/colors.png
88
+ - docs/samples/constructor.png
42
89
  - docs/samples/dither.png
43
90
  - docs/samples/draw.png
91
+ - docs/samples/iterator.png
44
92
  - docs/samples/paste.png
93
+ - docs/samples/pdither.png
94
+ - docs/samples/pipe.png
95
+ - docs/samples/range.png
96
+ - docs/samples/redimension.png
45
97
  - docs/samples/resize.png
46
98
  - docs/samples/sixel.png
99
+ - docs/samples/transform.png
100
+ - exe/image_util
47
101
  - lib/image_util.rb
102
+ - lib/image_util/benchmarking.rb
103
+ - lib/image_util/bitmap_font.rb
104
+ - lib/image_util/bitmap_font/fonts/smfont/charset.txt
105
+ - lib/image_util/bitmap_font/fonts/smfont/font.png
106
+ - lib/image_util/cli.rb
48
107
  - lib/image_util/codec.rb
49
108
  - lib/image_util/codec/_guard.rb
109
+ - lib/image_util/codec/chunky_png.rb
50
110
  - lib/image_util/codec/image_magick.rb
111
+ - lib/image_util/codec/kitty.rb
51
112
  - lib/image_util/codec/libpng.rb
52
113
  - lib/image_util/codec/libsixel.rb
53
114
  - lib/image_util/codec/libturbojpeg.rb
@@ -55,18 +116,28 @@ files:
55
116
  - lib/image_util/codec/ruby_sixel.rb
56
117
  - lib/image_util/color.rb
57
118
  - lib/image_util/color/css_colors.rb
119
+ - lib/image_util/extension.rb
58
120
  - lib/image_util/filter.rb
59
121
  - lib/image_util/filter/_mixin.rb
60
122
  - lib/image_util/filter/background.rb
61
- - lib/image_util/filter/dither.rb
123
+ - lib/image_util/filter/bitmap_text.rb
124
+ - lib/image_util/filter/colors.rb
62
125
  - lib/image_util/filter/draw.rb
126
+ - lib/image_util/filter/palette.rb
63
127
  - lib/image_util/filter/paste.rb
128
+ - lib/image_util/filter/redimension.rb
64
129
  - lib/image_util/filter/resize.rb
130
+ - lib/image_util/filter/transform.rb
131
+ - lib/image_util/generator.rb
132
+ - lib/image_util/generator/bitmap_text.rb
133
+ - lib/image_util/generator/example.rb
134
+ - lib/image_util/generator/example/rose.png
65
135
  - lib/image_util/image.rb
66
136
  - lib/image_util/image/buffer.rb
67
137
  - lib/image_util/magic.rb
68
138
  - lib/image_util/statistic.rb
69
- - lib/image_util/statistic/color.rb
139
+ - lib/image_util/statistic/colors.rb
140
+ - lib/image_util/terminal.rb
70
141
  - lib/image_util/util.rb
71
142
  - lib/image_util/version.rb
72
143
  - lib/image_util/view.rb
@@ -1,96 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ImageUtil
4
- module Filter
5
- module Dither
6
- extend ImageUtil::Filter::Mixin
7
-
8
- private
9
-
10
- def dither_distance_sq(c1, c2)
11
- len = [c1.length, c2.length].max
12
-
13
- case len
14
- when 1
15
- d = (c1[0] || 255) - (c2[0] || 255)
16
- d * d
17
- when 2
18
- d0 = (c1[0] || 255) - (c2[0] || 255)
19
- d1 = (c1[1] || 255) - (c2[1] || 255)
20
- d0 * d0 + d1 * d1
21
- when 3
22
- d0 = (c1[0] || 255) - (c2[0] || 255)
23
- d1 = (c1[1] || 255) - (c2[1] || 255)
24
- d2 = (c1[2] || 255) - (c2[2] || 255)
25
- d0 * d0 + d1 * d1 + d2 * d2
26
- when 4
27
- d0 = (c1[0] || 255) - (c2[0] || 255)
28
- d1 = (c1[1] || 255) - (c2[1] || 255)
29
- d2 = (c1[2] || 255) - (c2[2] || 255)
30
- d3 = (c1[3] || 255) - (c2[3] || 255)
31
- d0 * d0 + d1 * d1 + d2 * d2 + d3 * d3
32
- else
33
- sum = 0
34
- len.times do |i|
35
- d = (c1[i] || 255) - (c2[i] || 255)
36
- sum += d * d
37
- end
38
- sum
39
- end
40
- end
41
-
42
- public
43
-
44
- def dither!(count)
45
- palette = histogram.sort_by { |_, v| -v - rand }.first(count).map(&:first)
46
-
47
- cache = {}
48
-
49
- nearest = lambda do |color|
50
- key = (color[0] || 255) |
51
- ((color[1] || 255) << 8) |
52
- ((color[2] || 255) << 16) |
53
- ((color[3] || 255) << 24)
54
- cache[key] ||= begin
55
- best = palette.first
56
- best_dist = dither_distance_sq(color, best)
57
- idx = 1
58
- while idx < palette.length
59
- c = palette[idx]
60
- dist = dither_distance_sq(color, c)
61
- if dist < best_dist
62
- best = c
63
- best_dist = dist
64
- end
65
- idx += 1
66
- end
67
- best
68
- end
69
- end
70
-
71
- if dimensions.length == 2
72
- w = width
73
- h = height
74
- buf = buffer
75
- idx = 0
76
- step = buf.pixel_bytes
77
- h.times do
78
- w.times do
79
- color = buf.get_index(idx)
80
- buf.set_index(idx, nearest.call(color))
81
- idx += step
82
- end
83
- end
84
- else
85
- set_each_pixel_by_location do |loc|
86
- color = self[*loc]
87
- nearest.call(color)
88
- end
89
- end
90
- self
91
- end
92
-
93
- define_immutable_version :dither
94
- end
95
- end
96
- end