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,7 +3,7 @@
3
3
  module ImageUtil
4
4
  module Codec
5
5
  module ImageMagick
6
- SUPPORTED_FORMATS = %i[sixel jpeg png].freeze
6
+ SUPPORTED_FORMATS = %i[sixel jpeg png gif apng].freeze
7
7
 
8
8
  extend Guard
9
9
 
@@ -15,43 +15,101 @@ module ImageUtil
15
15
  @magick_available = system("magick", "-version", out: File::NULL, err: File::NULL)
16
16
  end
17
17
 
18
- def supported?(format = nil)
18
+ # Some codecs, like APNG won't work on Windows :(
19
+ def win_platform? = Gem.win_platform?
20
+
21
+ def magick_formats
22
+ return @magick_formats if defined?(@magick_formats)
23
+
24
+ out = IO.popen(%w[magick -list format], &:read)
25
+ @magick_formats = out.lines.filter_map do |line|
26
+ next unless line.start_with?(" ")
27
+
28
+ fmt = line[0,9].gsub(" ", "").downcase
29
+ fmt.empty? ? nil : fmt
30
+
31
+ next unless fmt
32
+
33
+ [fmt, line[21,2]]
34
+ end.to_h
35
+ @magick_formats
36
+ rescue StandardError
37
+ @magick_formats = {}
38
+ end
39
+
40
+ def supported?(format = nil, direction = "rw")
19
41
  return false unless magick_available?
20
42
 
21
43
  return true if format.nil?
22
44
 
23
- SUPPORTED_FORMATS.include?(format.to_s.downcase.to_sym)
45
+ return false if win_platform? && format == :apng
46
+
47
+ fmt = format.to_s.downcase
48
+ SUPPORTED_FORMATS.include?(fmt.to_sym) && magick_formats.key?(fmt) &&
49
+ magick_formats[fmt].include?(direction.to_s)
24
50
  end
25
51
 
26
52
  def encode(format, image)
27
53
  guard_supported_format!(format, SUPPORTED_FORMATS)
28
54
 
29
55
  fmt = format.to_s.downcase
30
- pam = Codec::Pam.encode(:pam, image, fill_to: fmt == "sixel" ? 6 : nil)
31
56
 
32
- IO.popen(["magick", "pam:-", "#{fmt}:-"], "r+") do |proc_io|
33
- proc_io << pam
34
- proc_io.close_write
35
- proc_io.read
36
- end
37
- end
57
+ if image.dimensions.length <= 2 || fmt == "sixel"
58
+ img = image
59
+ if img.dimensions.length == 1
60
+ img = img.redimension(img.width, 1)
61
+ end
62
+ if fmt == "sixel"
63
+ pad = (6 - (img.height % 6)) % 6
64
+ img = img.redimension(img.width, img.height + pad) if pad > 0
65
+ end
66
+ pam = Codec::Pam.encode(:pam, img)
38
67
 
39
- def encode_io(format, image, io)
40
- io << encode(format, image)
68
+ IO.popen(["magick", "pam:-", "#{fmt}:-"], "r+b") do |proc_io|
69
+ proc_io << pam
70
+ proc_io.close_write
71
+ proc_io.read
72
+ end
73
+ else
74
+ frames = image.buffer.last_dimension_split.map { |b| Image.from_buffer(b) }
75
+ stream = frames.map { |f| Codec::Pam.encode(:pam, f) }.join
76
+ IO.popen(["magick", "pam:-", "#{fmt}:-"], "r+b") do |proc_io|
77
+ proc_io << stream
78
+ proc_io.close_write
79
+ proc_io.read
80
+ end
81
+ end
41
82
  end
42
83
 
43
84
  def decode(format, data)
44
85
  guard_supported_format!(format, SUPPORTED_FORMATS)
45
86
 
46
- IO.popen(["magick", "#{format}:-", "pam:-"], "r+") do |proc_io|
87
+ cmd = ["magick", "#{format}:-"]
88
+ cmd << "-coalesce" if %i[gif apng].include?(format.to_s.downcase.to_sym)
89
+ cmd += ["-depth", "8", "pam:-"]
90
+ IO.popen(cmd, "r+b") do |proc_io|
47
91
  proc_io << data
48
92
  proc_io.close_write
49
- Pam.decode(:pam, proc_io.read)
50
- end
51
- end
52
93
 
53
- def decode_io(format, io)
54
- decode(format, io.read)
94
+ frames = []
95
+ while (frame = Codec::Pam.decode_frame(proc_io))
96
+ frames << frame
97
+ end
98
+
99
+ if frames.length == 1
100
+ frames.first
101
+ else
102
+ first = frames.first
103
+ img = Image.new(first.width, first.height, frames.length,
104
+ color_bits: first.color_bits, channels: first.channels)
105
+ frames.each_with_index do |frame, idx|
106
+ offset = img.buffer.offset_of(0, 0, idx)
107
+ bytes = frame.width * frame.height * frame.pixel_bytes
108
+ img.buffer.io_buffer.copy(frame.buffer.io_buffer, offset, bytes)
109
+ end
110
+ img
111
+ end
112
+ end
55
113
  end
56
114
  end
57
115
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module ImageUtil
6
+ module Codec
7
+ module Kitty
8
+ SUPPORTED_FORMATS = [:kitty].freeze
9
+
10
+ extend Guard
11
+
12
+ module_function
13
+
14
+ def supported?(format = nil)
15
+ return true if format.nil?
16
+
17
+ SUPPORTED_FORMATS.include?(format.to_s.downcase.to_sym)
18
+ end
19
+
20
+ # Kitty format supports more options:
21
+ # https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
22
+ def encode(format, image, options: nil)
23
+ guard_supported_format!(format, SUPPORTED_FORMATS)
24
+ guard_image_class!(image)
25
+ guard_8bit_colors!(image)
26
+ raise ArgumentError, "only 1d or 2d images supported" if image.dimensions.length > 2
27
+
28
+ img = image
29
+ img = img.redimension(img.width, 1) if img.dimensions.length == 1
30
+
31
+ bits = img.pixel_bytes * 8
32
+ width = img.width
33
+ height = img.height
34
+
35
+ rest = Base64.strict_encode64(img.buffer.get_string)
36
+
37
+ first = true
38
+
39
+ out = +""
40
+
41
+ options ||= begin
42
+ opts = {}
43
+ opts[:a] = "T" # immediately display
44
+ opts[:q] = 2 # don't report anything
45
+ opts
46
+ end
47
+
48
+ loop do
49
+ payload, rest = rest[...4096], rest[4096..] # rubocop:disable Style/ParallelAssignment
50
+
51
+ opts = {}
52
+
53
+ if first
54
+ opts = opts.merge(options)
55
+ opts[:f] = bits
56
+ opts[:s] = width
57
+ opts[:v] = height
58
+ opts[:m] = 1 if rest
59
+ elsif rest
60
+ opts[:m] = 1
61
+ else
62
+ opts[:m] = 0
63
+ end
64
+
65
+ opts = opts.map { |k,v| "#{k}=#{v}" }.join(",")
66
+
67
+ out << "\e_G#{opts};#{payload}\e\\".b
68
+
69
+ first = false
70
+ break unless rest
71
+ end
72
+
73
+ out
74
+ end
75
+
76
+ def decode(*)
77
+ raise UnsupportedFormatError, "decode not supported for sixel"
78
+ end
79
+ end
80
+ end
81
+ end
@@ -69,7 +69,7 @@ module ImageUtil
69
69
  guard_2d_image!(image)
70
70
  guard_8bit_colors!(image)
71
71
 
72
- fmt = if image.color_length == 4
72
+ fmt = if image.channels == 4
73
73
  PNG_FORMAT_RGBA
74
74
  else
75
75
  PNG_FORMAT_RGB
@@ -83,7 +83,7 @@ module ImageUtil
83
83
  img[:flags] = 0
84
84
  img[:colormap_entries] = 0
85
85
 
86
- row_stride = image.width * image.color_length
86
+ row_stride = image.width * image.channels
87
87
  buffer_ptr = FFI::MemoryPointer.from_string(image.buffer.get_string)
88
88
  size_ptr = FFI::MemoryPointer.new(:size_t)
89
89
 
@@ -100,10 +100,6 @@ module ImageUtil
100
100
  png_image_free(img) if img
101
101
  end
102
102
 
103
- def encode_io(format, image, io)
104
- io << encode(format, image)
105
- end
106
-
107
103
  def decode(format, data)
108
104
  guard_supported_format!(format, SUPPORTED_FORMATS)
109
105
  raise UnsupportedFormatError, "libpng not available" unless AVAILABLE
@@ -129,10 +125,6 @@ module ImageUtil
129
125
  ensure
130
126
  png_image_free(img) if img
131
127
  end
132
-
133
- def decode_io(format, io)
134
- decode(format, io.read)
135
- end
136
128
  end
137
129
  end
138
130
  end
@@ -56,10 +56,18 @@ module ImageUtil
56
56
  guard_supported_format!(format, SUPPORTED_FORMATS)
57
57
  raise UnsupportedFormatError, "libsixel not available" unless AVAILABLE
58
58
 
59
- guard_2d_image!(image)
59
+ guard_image_class!(image)
60
60
  guard_8bit_colors!(image)
61
+ raise ArgumentError, "only 1d or 2d images supported" if image.dimensions.length > 2
61
62
 
62
- fmt = image.color_length == 4 ? SIXEL_PIXELFORMAT_RGBA8888 : SIXEL_PIXELFORMAT_RGB888
63
+ img = image
64
+ if img.dimensions.length == 1
65
+ img = img.redimension(img.width, 1)
66
+ end
67
+ pad = (6 - (img.height % 6)) % 6
68
+ img = img.redimension(img.width, img.height + pad) if pad > 0
69
+
70
+ fmt = img.channels == 4 ? SIXEL_PIXELFORMAT_RGBA8888 : SIXEL_PIXELFORMAT_RGB888
63
71
 
64
72
  data = "".b
65
73
  writer = FFI::Function.new(:int, %i[pointer int pointer]) do |ptr, size, _|
@@ -79,15 +87,15 @@ module ImageUtil
79
87
 
80
88
  dither = dither_ptr.read_pointer
81
89
 
82
- pixels = image.buffer.get_string
90
+ pixels = img.buffer.get_string
83
91
  buf_ptr = FFI::MemoryPointer.new(:uchar, pixels.bytesize)
84
92
  buf_ptr.put_bytes(0, pixels)
85
93
 
86
94
  res = sixel_dither_initialize(
87
95
  dither,
88
96
  buf_ptr,
89
- image.width,
90
- image.height,
97
+ img.width,
98
+ img.height,
91
99
  fmt,
92
100
  SIXEL_LARGE_AUTO,
93
101
  SIXEL_REP_AUTO,
@@ -97,7 +105,7 @@ module ImageUtil
97
105
 
98
106
  sixel_dither_set_diffusion_type(dither, SIXEL_DIFFUSE_AUTO)
99
107
 
100
- res = sixel_encode(buf_ptr, image.width, image.height, fmt, dither, output)
108
+ res = sixel_encode(buf_ptr, img.width, img.height, fmt, dither, output)
101
109
  raise StandardError, "sixel_encode failed" if res != 0
102
110
 
103
111
  data
@@ -106,17 +114,9 @@ module ImageUtil
106
114
  sixel_output_unref(output) if defined?(output) && output && !output.null?
107
115
  end
108
116
 
109
- def encode_io(format, image, io)
110
- io << encode(format, image)
111
- end
112
-
113
117
  def decode(*)
114
118
  raise UnsupportedFormatError, "decode not supported for sixel"
115
119
  end
116
-
117
- def decode_io(*)
118
- raise UnsupportedFormatError, "decode not supported for sixel"
119
- end
120
120
  end
121
121
  end
122
122
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  module ImageUtil
4
4
  module Codec
5
- # rubocop:disable Metrics/ModuleLength
6
5
  module Libturbojpeg
7
6
  SUPPORTED_FORMATS = [:jpeg].freeze
8
7
 
@@ -70,7 +69,7 @@ module ImageUtil
70
69
  guard_2d_image!(image)
71
70
  guard_8bit_colors!(image)
72
71
 
73
- fmt = image.color_length == 4 ? TJPF_RGBA : TJPF_RGB
72
+ fmt = image.channels == 4 ? TJPF_RGBA : TJPF_RGB
74
73
 
75
74
  handle = tjInitCompress
76
75
  raise StandardError, "tjInitCompress failed" if handle.null?
@@ -91,10 +90,6 @@ module ImageUtil
91
90
  tjDestroy(handle) if handle && !handle.null?
92
91
  end
93
92
 
94
- def encode_io(format, image, io, **kwargs)
95
- io << encode(format, image, **kwargs)
96
- end
97
-
98
93
  def decode(format, data)
99
94
  guard_supported_format!(format, SUPPORTED_FORMATS)
100
95
  raise UnsupportedFormatError, "libturbojpeg not available" unless AVAILABLE
@@ -131,11 +126,6 @@ module ImageUtil
131
126
  ensure
132
127
  tjDestroy(handle) if handle && !handle.null?
133
128
  end
134
-
135
- def decode_io(format, io)
136
- decode(format, io.read)
137
- end
138
129
  end
139
- # rubocop:enable Metrics/ModuleLength
140
130
  end
141
131
  end
@@ -17,40 +17,29 @@ module ImageUtil
17
17
  SUPPORTED_FORMATS.include?(format.to_s.downcase.to_sym)
18
18
  end
19
19
 
20
- def encode(format, image, fill_to: nil)
20
+ def encode(format, image)
21
21
  guard_supported_format!(format, SUPPORTED_FORMATS)
22
22
  unless image.dimensions.length <= 2
23
23
  raise ArgumentError, "can't convert to PAM more than 2 dimensions"
24
24
  end
25
25
 
26
- unless [3, 4].include?(image.color_length)
26
+ unless [3, 4].include?(image.channels)
27
27
  raise ArgumentError, "can't convert to PAM if color length isn't 3 or 4"
28
28
  end
29
29
 
30
- fill_height = image.height || 1
31
- fill_buffer = "".b
32
- if fill_to
33
- remaining = fill_height % fill_to
34
- added = remaining.zero? ? 0 : fill_to - remaining
35
- fill_height += added
36
- fill_buffer = "\0".b * added * image.pixel_bytes * image.width
37
- end
30
+ height = image.height || 1
38
31
 
39
32
  header = <<~PAM.b
40
33
  P7
41
34
  WIDTH #{image.width}
42
- HEIGHT #{fill_height}
43
- DEPTH #{image.color_length}
35
+ HEIGHT #{height}
36
+ DEPTH #{image.channels}
44
37
  MAXVAL #{2**image.color_bits - 1}
45
- TUPLTYPE #{image.color_length == 3 ? "RGB" : "RGB_ALPHA"}
38
+ TUPLTYPE #{image.channels == 3 ? "RGB" : "RGB_ALPHA"}
46
39
  ENDHDR
47
40
  PAM
48
41
 
49
- header + image.buffer.get_string + fill_buffer
50
- end
51
-
52
- def encode_io(format, image, io, **kwargs)
53
- io << encode(format, image, **kwargs)
42
+ header + image.buffer.get_string
54
43
  end
55
44
 
56
45
  def decode(format, data)
@@ -62,20 +51,33 @@ module ImageUtil
62
51
  def decode_io(format, io)
63
52
  guard_supported_format!(format, SUPPORTED_FORMATS)
64
53
 
54
+ decode_frame(io)
55
+ end
56
+
57
+ def decode_frame(io)
65
58
  header = {}
66
- while (line = io.gets)
67
- line = line.chomp
68
- break if line == "ENDHDR"
59
+ line = io.gets&.chomp
60
+ return nil unless line
61
+
62
+ line = io.gets&.chomp
63
+ return nil unless line
69
64
 
65
+ until line.chomp == "ENDHDR"
70
66
  key, val = line.split(" ", 2)
71
67
  header[key] = val
68
+ line = io.gets&.chomp
69
+ return nil unless line
72
70
  end
71
+
73
72
  width = header["WIDTH"].to_i
74
73
  height = header["HEIGHT"].to_i
75
74
  depth = header["DEPTH"].to_i
76
75
  maxval = header["MAXVAL"].to_i
77
76
  color_bits = Math.log2(maxval + 1).to_i
78
- raw = io.read
77
+ bytes = width * height * depth * (color_bits / 8)
78
+ raw = io.read(bytes)
79
+ return nil unless raw && raw.bytesize == bytes
80
+
79
81
  io_buf = IO::Buffer.for(raw)
80
82
  buf = Image::Buffer.new([width, height], color_bits, depth, io_buf)
81
83
  Image.from_buffer(buf)
@@ -18,13 +18,20 @@ module ImageUtil
18
18
 
19
19
  def encode(format, image)
20
20
  guard_supported_format!(format, SUPPORTED_FORMATS)
21
- guard_2d_image!(image)
21
+ guard_image_class!(image)
22
22
  guard_8bit_colors!(image)
23
+ raise ArgumentError, "only 1d or 2d images supported" if image.dimensions.length > 2
23
24
 
24
- img = if image.unique_color_count <= 256
25
- image
25
+ img = image
26
+ if img.dimensions.length == 1
27
+ img = img.redimension(img.width, 1)
28
+ end
29
+ pad = (6 - (img.height % 6)) % 6
30
+ img = img.redimension(img.width, img.height + pad) if pad > 0
31
+ img = if img.unique_color_count <= 256
32
+ img
26
33
  else
27
- image.dither(256)
34
+ img.palette_reduce(256)
28
35
  end
29
36
 
30
37
  height = img.height || 1
@@ -100,17 +107,9 @@ module ImageUtil
100
107
  out
101
108
  end
102
109
 
103
- def encode_io(format, image, io)
104
- io << encode(format, image)
105
- end
106
-
107
110
  def decode(*)
108
111
  raise UnsupportedFormatError, "decode not supported for sixel"
109
112
  end
110
-
111
- def decode_io(*)
112
- raise UnsupportedFormatError, "decode not supported for sixel"
113
- end
114
113
  end
115
114
  end
116
115
  end
@@ -101,17 +101,21 @@ module ImageUtil
101
101
  autoload :Guard, "image_util/codec/_guard"
102
102
 
103
103
  autoload :Libpng, "image_util/codec/libpng"
104
+ autoload :ChunkyPng, "image_util/codec/chunky_png"
104
105
  autoload :Libturbojpeg, "image_util/codec/libturbojpeg"
105
106
  autoload :Pam, "image_util/codec/pam"
106
107
  autoload :Libsixel, "image_util/codec/libsixel"
107
108
  autoload :ImageMagick, "image_util/codec/image_magick"
108
109
  autoload :RubySixel, "image_util/codec/ruby_sixel"
110
+ autoload :Kitty, "image_util/codec/kitty"
109
111
 
110
112
  register_codec :Pam, :pam
113
+ register_codec :Kitty, :kitty
111
114
  register_codec :Libpng, :png
112
115
  register_codec :Libturbojpeg, :jpeg
113
116
  register_encoder :Libsixel, :sixel
114
- register_codec :ImageMagick, :png, :jpeg, :sixel
117
+ register_codec :ImageMagick, :png, :jpeg, :sixel, :gif, :apng
118
+ register_codec :ChunkyPng, :png
115
119
  register_encoder :RubySixel, :sixel
116
120
  end
117
121
  end
@@ -151,6 +151,8 @@ module ImageUtil
151
151
  "yellow" => [255, 255, 0],
152
152
  "yellowgreen" => [154, 205, 50],
153
153
  "rebeccapurple" => [102, 51, 153]
154
- }.freeze
154
+ }.transform_values(&:freeze).transform_keys(&:to_sym).freeze
155
+
156
+ CSS_COLORS_4C = CSS_COLORS.transform_values { |i| (i + [255]).freeze }.freeze
155
157
  end
156
158
  end
@@ -41,7 +41,7 @@ module ImageUtil
41
41
  end.then { |val| new(*val) }
42
42
  end
43
43
 
44
- def to_buffer(color_bits, color_length)
44
+ def to_buffer(color_bits, channels)
45
45
  map do |i|
46
46
  case color_bits
47
47
  when 8
@@ -49,9 +49,47 @@ module ImageUtil
49
49
  else
50
50
  (i.to_f * 2**(color_bits - 8)).to_i
51
51
  end
52
- end + [255] * (color_length - length)
52
+ end + [255] * (channels - length)
53
53
  end
54
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
+
55
93
  def self.from(value)
56
94
  case value
57
95
  when Color
@@ -71,15 +109,19 @@ module ImageUtil
71
109
  when /\A#(\h{2})(\h{2})(\h{2})(\h{2})\z/
72
110
  new($1.to_i(16), $2.to_i(16), $3.to_i(16), $4.to_i(16))
73
111
  else
74
- if (rgb = CSS_COLORS[value.downcase])
112
+ if (rgb = CSS_COLORS[value.downcase.to_sym])
75
113
  new(*rgb)
76
114
  else
77
115
  raise ArgumentError, "wrong String passed as color (passed: #{value.inspect})"
78
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