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
@@ -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
@@ -3,7 +3,7 @@
3
3
  module ImageUtil
4
4
  module Codec
5
5
  module ImageMagick
6
- SUPPORTED_FORMATS = [:sixel].freeze
6
+ SUPPORTED_FORMATS = %i[sixel jpeg png gif apng].freeze
7
7
 
8
8
  extend Guard
9
9
 
@@ -15,34 +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
- IO.popen("magick pam:- sixel:-", "r+") do |io|
30
- io << Codec::Pam.encode(:pam, image, fill_to: 6)
31
- io.close_write
32
- io.read
55
+ fmt = format.to_s.downcase
56
+
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)
67
+
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
33
81
  end
34
82
  end
35
83
 
36
- def encode_io(format, image, io)
37
- io << encode(format, image)
38
- end
84
+ def decode(format, data)
85
+ guard_supported_format!(format, SUPPORTED_FORMATS)
39
86
 
40
- def decode(*)
41
- raise UnsupportedFormatError, "decode not supported for sixel"
42
- end
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|
91
+ proc_io << data
92
+ proc_io.close_write
93
+
94
+ frames = []
95
+ while (frame = Codec::Pam.decode_frame(proc_io))
96
+ frames << frame
97
+ end
43
98
 
44
- def decode_io(*)
45
- raise UnsupportedFormatError, "decode not supported for sixel"
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
46
113
  end
47
114
  end
48
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
@@ -56,7 +63,7 @@ module ImageUtil
56
63
  end
57
64
  end
58
65
 
59
- out = "\ePq".dup
66
+ out = "\ePq\"1;1;#{width};#{height}".dup
60
67
  palette.each_with_index do |c, idx|
61
68
  out << format("#%d;2;%d;%d;%d", idx, c.r * 100 / 255, c.g * 100 / 255, c.b * 100 / 255)
62
69
  end
@@ -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_encoder :ImageMagick, :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