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.
- checksums.yaml +4 -4
- data/AGENTS.md +3 -0
- data/CHANGELOG.md +31 -0
- data/README.md +125 -24
- 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/iterator.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/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 +76 -18
- 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 +11 -12
- data/lib/image_util/codec.rb +5 -1
- data/lib/image_util/color/css_colors.rb +3 -1
- data/lib/image_util/color.rb +62 -9
- 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 +2 -11
- 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 +49 -23
- 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 +75 -4
- 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
|
-
|
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
55
|
fmt = format.to_s.downcase
|
30
|
-
pam = Codec::Pam.encode(:pam, image, fill_to: fmt == "sixel" ? 6 : nil)
|
31
56
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
40
|
-
|
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
|
-
|
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
|
-
|
54
|
-
|
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.
|
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
|
@@ -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
|
-
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
|
data/lib/image_util/color.rb
CHANGED
@@ -41,7 +41,7 @@ module ImageUtil
|
|
41
41
|
end.then { |val| new(*val) }
|
42
42
|
end
|
43
43
|
|
44
|
-
def to_buffer(color_bits,
|
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] * (
|
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
|
-
|
82
|
-
|
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
|
-
#
|
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
|
-
|
145
|
-
|
146
|
-
|
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
|
@@ -4,15 +4,15 @@ module ImageUtil
|
|
4
4
|
module Filter
|
5
5
|
module Background
|
6
6
|
def background(bgcolor)
|
7
|
-
return self if
|
7
|
+
return self if channels == 3
|
8
8
|
|
9
|
-
unless
|
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,
|
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
|