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.
- checksums.yaml +4 -4
- data/AGENTS.md +5 -6
- data/CHANGELOG.md +41 -6
- data/README.md +229 -81
- data/Rakefile +5 -0
- data/docs/cli.md +5 -0
- data/docs/samples/background.png +0 -0
- data/docs/samples/bitmap_text.png +0 -0
- data/docs/samples/colors.png +0 -0
- data/docs/samples/constructor.png +0 -0
- data/docs/samples/dither.png +0 -0
- data/docs/samples/draw.png +0 -0
- data/docs/samples/iterator.png +0 -0
- data/docs/samples/paste.png +0 -0
- data/docs/samples/pdither.png +0 -0
- data/docs/samples/pipe.png +0 -0
- data/docs/samples/range.png +0 -0
- data/docs/samples/redimension.png +0 -0
- data/docs/samples/resize.png +0 -0
- data/docs/samples/sixel.png +0 -0
- data/docs/samples/transform.png +0 -0
- data/exe/image_util +7 -0
- data/lib/image_util/benchmarking.rb +25 -0
- data/lib/image_util/bitmap_font/fonts/smfont/charset.txt +1 -0
- data/lib/image_util/bitmap_font/fonts/smfont/font.png +0 -0
- data/lib/image_util/bitmap_font.rb +72 -0
- data/lib/image_util/cli.rb +54 -0
- data/lib/image_util/codec/chunky_png.rb +67 -0
- data/lib/image_util/codec/image_magick.rb +82 -15
- data/lib/image_util/codec/kitty.rb +81 -0
- data/lib/image_util/codec/libpng.rb +2 -10
- data/lib/image_util/codec/libsixel.rb +14 -14
- data/lib/image_util/codec/libturbojpeg.rb +1 -11
- data/lib/image_util/codec/pam.rb +24 -22
- data/lib/image_util/codec/ruby_sixel.rb +12 -13
- data/lib/image_util/codec.rb +5 -1
- data/lib/image_util/color/css_colors.rb +158 -0
- data/lib/image_util/color.rb +67 -14
- data/lib/image_util/extension.rb +24 -0
- data/lib/image_util/filter/_mixin.rb +9 -0
- data/lib/image_util/filter/background.rb +4 -4
- data/lib/image_util/filter/bitmap_text.rb +17 -0
- data/lib/image_util/filter/colors.rb +21 -0
- data/lib/image_util/filter/draw.rb +22 -9
- data/lib/image_util/filter/palette.rb +197 -0
- data/lib/image_util/filter/paste.rb +1 -1
- data/lib/image_util/filter/redimension.rb +83 -0
- data/lib/image_util/filter/resize.rb +1 -1
- data/lib/image_util/filter/transform.rb +48 -0
- data/lib/image_util/filter.rb +5 -1
- data/lib/image_util/generator/bitmap_text.rb +38 -0
- data/lib/image_util/generator/example/rose.png +0 -0
- data/lib/image_util/generator/example.rb +9 -0
- data/lib/image_util/generator.rb +8 -0
- data/lib/image_util/image/buffer.rb +11 -11
- data/lib/image_util/image.rb +54 -26
- data/lib/image_util/magic.rb +8 -6
- data/lib/image_util/statistic/{color.rb → colors.rb} +2 -2
- data/lib/image_util/statistic.rb +1 -1
- data/lib/image_util/terminal.rb +61 -0
- data/lib/image_util/version.rb +1 -1
- data/lib/image_util/view/interpolated.rb +1 -1
- data/lib/image_util.rb +6 -0
- metadata +82 -4
- 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 = [
|
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
|
-
|
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
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
37
|
-
|
38
|
-
end
|
84
|
+
def decode(format, data)
|
85
|
+
guard_supported_format!(format, SUPPORTED_FORMATS)
|
39
86
|
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
45
|
-
|
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.
|
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.
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
90
|
-
|
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,
|
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.
|
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
|
data/lib/image_util/codec/pam.rb
CHANGED
@@ -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
|
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.
|
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
|
-
|
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 #{
|
43
|
-
DEPTH #{image.
|
35
|
+
HEIGHT #{height}
|
36
|
+
DEPTH #{image.channels}
|
44
37
|
MAXVAL #{2**image.color_bits - 1}
|
45
|
-
TUPLTYPE #{image.
|
38
|
+
TUPLTYPE #{image.channels == 3 ? "RGB" : "RGB_ALPHA"}
|
46
39
|
ENDHDR
|
47
40
|
PAM
|
48
41
|
|
49
|
-
header + image.buffer.get_string
|
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
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
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
|
-
|
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 =
|
25
|
-
|
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
|
-
|
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
|
data/lib/image_util/codec.rb
CHANGED
@@ -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
|
-
|
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
|