image_util 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +96 -0
- data/AGENTS.md +16 -0
- data/CHANGELOG.md +18 -0
- data/LICENSE.txt +21 -0
- data/README.md +157 -0
- data/Rakefile +10 -0
- data/lib/image_util/codec/_guard.rb +35 -0
- data/lib/image_util/codec/image_magick.rb +49 -0
- data/lib/image_util/codec/libpng.rb +138 -0
- data/lib/image_util/codec/libsixel.rb +122 -0
- data/lib/image_util/codec/libturbojpeg.rb +141 -0
- data/lib/image_util/codec/pam.rb +85 -0
- data/lib/image_util/codec/ruby_sixel.rb +116 -0
- data/lib/image_util/codec.rb +117 -0
- data/lib/image_util/color.rb +149 -0
- data/lib/image_util/filter/_mixin.rb +15 -0
- data/lib/image_util/filter/background.rb +23 -0
- data/lib/image_util/filter/dither.rb +96 -0
- data/lib/image_util/filter/draw.rb +77 -0
- data/lib/image_util/filter/paste.rb +54 -0
- data/lib/image_util/filter/resize.rb +20 -0
- data/lib/image_util/filter.rb +13 -0
- data/lib/image_util/image/buffer.rb +133 -0
- data/lib/image_util/image.rb +242 -0
- data/lib/image_util/magic.rb +45 -0
- data/lib/image_util/statistic/color.rb +9 -0
- data/lib/image_util/statistic.rb +7 -0
- data/lib/image_util/util.rb +15 -0
- data/lib/image_util/version.rb +5 -0
- data/lib/image_util/view/interpolated.rb +45 -0
- data/lib/image_util/view/rounded.rb +16 -0
- data/lib/image_util/view.rb +8 -0
- data/lib/image_util.rb +17 -0
- data/sig/image_util.rbs +4 -0
- metadata +96 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
module Codec
|
5
|
+
# rubocop:disable Metrics/ModuleLength
|
6
|
+
module Libturbojpeg
|
7
|
+
SUPPORTED_FORMATS = [:jpeg].freeze
|
8
|
+
|
9
|
+
extend Guard
|
10
|
+
|
11
|
+
begin
|
12
|
+
require "ffi"
|
13
|
+
|
14
|
+
extend FFI::Library
|
15
|
+
ffi_lib [
|
16
|
+
"libturbojpeg.so.0", # Linux
|
17
|
+
"libturbojpeg.so", "libturbojpeg", # generic
|
18
|
+
"turbojpeg.dll", "libturbojpeg.dll", # Windows
|
19
|
+
"libturbojpeg.dylib", "turbojpeg.dylib" # macOS
|
20
|
+
]
|
21
|
+
|
22
|
+
AVAILABLE = true
|
23
|
+
rescue LoadError
|
24
|
+
AVAILABLE = false
|
25
|
+
end
|
26
|
+
|
27
|
+
TJPF_RGB = 0
|
28
|
+
TJPF_RGBA = 7
|
29
|
+
|
30
|
+
if AVAILABLE
|
31
|
+
attach_function :tjInitCompress, [], :pointer
|
32
|
+
attach_function :tjInitDecompress, [], :pointer
|
33
|
+
attach_function :tjCompress2,
|
34
|
+
%i[pointer pointer int int int int pointer pointer int int int],
|
35
|
+
:int
|
36
|
+
|
37
|
+
begin
|
38
|
+
attach_function :tjDecompressHeader3,
|
39
|
+
%i[pointer pointer ulong pointer pointer pointer pointer],
|
40
|
+
:int
|
41
|
+
DECOMPRESS_HEADER_FUNC = :tjDecompressHeader3
|
42
|
+
rescue FFI::NotFoundError
|
43
|
+
attach_function :tjDecompressHeader2,
|
44
|
+
%i[pointer pointer ulong pointer pointer pointer],
|
45
|
+
:int
|
46
|
+
DECOMPRESS_HEADER_FUNC = :tjDecompressHeader2
|
47
|
+
end
|
48
|
+
|
49
|
+
attach_function :tjDecompress2,
|
50
|
+
%i[pointer pointer ulong pointer int int int int int],
|
51
|
+
:int
|
52
|
+
attach_function :tjDestroy, [:pointer], :int
|
53
|
+
attach_function :tjFree, [:pointer], :void
|
54
|
+
end
|
55
|
+
|
56
|
+
module_function
|
57
|
+
|
58
|
+
def supported?(format = nil)
|
59
|
+
return false unless AVAILABLE
|
60
|
+
|
61
|
+
return true if format.nil?
|
62
|
+
|
63
|
+
SUPPORTED_FORMATS.include?(format.to_s.downcase.to_sym)
|
64
|
+
end
|
65
|
+
|
66
|
+
def encode(format, image, quality: 75)
|
67
|
+
guard_supported_format!(format, SUPPORTED_FORMATS)
|
68
|
+
raise UnsupportedFormatError, "libturbojpeg not available" unless AVAILABLE
|
69
|
+
|
70
|
+
guard_2d_image!(image)
|
71
|
+
guard_8bit_colors!(image)
|
72
|
+
|
73
|
+
fmt = image.color_length == 4 ? TJPF_RGBA : TJPF_RGB
|
74
|
+
|
75
|
+
handle = tjInitCompress
|
76
|
+
raise StandardError, "tjInitCompress failed" if handle.null?
|
77
|
+
|
78
|
+
src_ptr = FFI::MemoryPointer.from_string(image.buffer.get_string)
|
79
|
+
jpeg_ptr_ptr = FFI::MemoryPointer.new(:pointer)
|
80
|
+
jpeg_size_ptr = FFI::MemoryPointer.new(:ulong)
|
81
|
+
|
82
|
+
res = tjCompress2(handle, src_ptr, image.width, 0, image.height, fmt, jpeg_ptr_ptr, jpeg_size_ptr, 0, quality, 0)
|
83
|
+
raise StandardError, "compression failed" if res != 0
|
84
|
+
|
85
|
+
jpeg_ptr = jpeg_ptr_ptr.read_pointer
|
86
|
+
jpeg_size = jpeg_size_ptr.read_ulong
|
87
|
+
data = jpeg_ptr.read_string(jpeg_size)
|
88
|
+
tjFree(jpeg_ptr)
|
89
|
+
data
|
90
|
+
ensure
|
91
|
+
tjDestroy(handle) if handle && !handle.null?
|
92
|
+
end
|
93
|
+
|
94
|
+
def encode_io(format, image, io, **kwargs)
|
95
|
+
io << encode(format, image, **kwargs)
|
96
|
+
end
|
97
|
+
|
98
|
+
def decode(format, data)
|
99
|
+
guard_supported_format!(format, SUPPORTED_FORMATS)
|
100
|
+
raise UnsupportedFormatError, "libturbojpeg not available" unless AVAILABLE
|
101
|
+
|
102
|
+
handle = tjInitDecompress
|
103
|
+
raise StandardError, "tjInitDecompress failed" if handle.null?
|
104
|
+
|
105
|
+
jpeg_buf = FFI::MemoryPointer.from_string(data)
|
106
|
+
width_ptr = FFI::MemoryPointer.new(:int)
|
107
|
+
height_ptr = FFI::MemoryPointer.new(:int)
|
108
|
+
subsamp_ptr = FFI::MemoryPointer.new(:int)
|
109
|
+
cs_ptr = FFI::MemoryPointer.new(:int)
|
110
|
+
|
111
|
+
header_args = [handle, jpeg_buf, data.bytesize, width_ptr, height_ptr]
|
112
|
+
if DECOMPRESS_HEADER_FUNC == :tjDecompressHeader3
|
113
|
+
header_args += [subsamp_ptr, cs_ptr]
|
114
|
+
else
|
115
|
+
header_args << subsamp_ptr
|
116
|
+
end
|
117
|
+
res = public_send(DECOMPRESS_HEADER_FUNC, *header_args)
|
118
|
+
raise StandardError, "header decode failed" if res != 0
|
119
|
+
|
120
|
+
width = width_ptr.read_int
|
121
|
+
height = height_ptr.read_int
|
122
|
+
|
123
|
+
dst_ptr = FFI::MemoryPointer.new(:uchar, width * height * 4)
|
124
|
+
res = tjDecompress2(handle, jpeg_buf, data.bytesize, dst_ptr, width, 0, height, TJPF_RGBA, 0)
|
125
|
+
raise StandardError, "decompress failed" if res != 0
|
126
|
+
|
127
|
+
raw = dst_ptr.read_string(width * height * 4)
|
128
|
+
io_buf = IO::Buffer.for(raw)
|
129
|
+
buf = Image::Buffer.new([width, height], 8, 4, io_buf)
|
130
|
+
Image.from_buffer(buf)
|
131
|
+
ensure
|
132
|
+
tjDestroy(handle) if handle && !handle.null?
|
133
|
+
end
|
134
|
+
|
135
|
+
def decode_io(format, io)
|
136
|
+
decode(format, io.read)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
# rubocop:enable Metrics/ModuleLength
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "stringio"
|
4
|
+
|
5
|
+
module ImageUtil
|
6
|
+
module Codec
|
7
|
+
module Pam
|
8
|
+
SUPPORTED_FORMATS = [:pam].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
|
+
def encode(format, image, fill_to: nil)
|
21
|
+
guard_supported_format!(format, SUPPORTED_FORMATS)
|
22
|
+
unless image.dimensions.length <= 2
|
23
|
+
raise ArgumentError, "can't convert to PAM more than 2 dimensions"
|
24
|
+
end
|
25
|
+
|
26
|
+
unless [3, 4].include?(image.color_length)
|
27
|
+
raise ArgumentError, "can't convert to PAM if color length isn't 3 or 4"
|
28
|
+
end
|
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
|
38
|
+
|
39
|
+
header = <<~PAM.b
|
40
|
+
P7
|
41
|
+
WIDTH #{image.width}
|
42
|
+
HEIGHT #{fill_height}
|
43
|
+
DEPTH #{image.color_length}
|
44
|
+
MAXVAL #{2**image.color_bits - 1}
|
45
|
+
TUPLTYPE #{image.color_length == 3 ? "RGB" : "RGB_ALPHA"}
|
46
|
+
ENDHDR
|
47
|
+
PAM
|
48
|
+
|
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)
|
54
|
+
end
|
55
|
+
|
56
|
+
def decode(format, data)
|
57
|
+
guard_supported_format!(format, SUPPORTED_FORMATS)
|
58
|
+
|
59
|
+
decode_io(format, StringIO.new(data))
|
60
|
+
end
|
61
|
+
|
62
|
+
def decode_io(format, io)
|
63
|
+
guard_supported_format!(format, SUPPORTED_FORMATS)
|
64
|
+
|
65
|
+
header = {}
|
66
|
+
while (line = io.gets)
|
67
|
+
line = line.chomp
|
68
|
+
break if line == "ENDHDR"
|
69
|
+
|
70
|
+
key, val = line.split(" ", 2)
|
71
|
+
header[key] = val
|
72
|
+
end
|
73
|
+
width = header["WIDTH"].to_i
|
74
|
+
height = header["HEIGHT"].to_i
|
75
|
+
depth = header["DEPTH"].to_i
|
76
|
+
maxval = header["MAXVAL"].to_i
|
77
|
+
color_bits = Math.log2(maxval + 1).to_i
|
78
|
+
raw = io.read
|
79
|
+
io_buf = IO::Buffer.for(raw)
|
80
|
+
buf = Image::Buffer.new([width, height], color_bits, depth, io_buf)
|
81
|
+
Image.from_buffer(buf)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
module Codec
|
5
|
+
module RubySixel
|
6
|
+
SUPPORTED_FORMATS = [:sixel].freeze
|
7
|
+
CHAR_MAP = (0..63).map { |b| (63 + b).chr }.freeze
|
8
|
+
|
9
|
+
extend Guard
|
10
|
+
|
11
|
+
module_function
|
12
|
+
|
13
|
+
def supported?(format = nil)
|
14
|
+
return true if format.nil?
|
15
|
+
|
16
|
+
SUPPORTED_FORMATS.include?(format.to_s.downcase.to_sym)
|
17
|
+
end
|
18
|
+
|
19
|
+
def encode(format, image)
|
20
|
+
guard_supported_format!(format, SUPPORTED_FORMATS)
|
21
|
+
guard_2d_image!(image)
|
22
|
+
guard_8bit_colors!(image)
|
23
|
+
|
24
|
+
img = if image.unique_color_count <= 256
|
25
|
+
image
|
26
|
+
else
|
27
|
+
image.dither(256)
|
28
|
+
end
|
29
|
+
|
30
|
+
height = img.height || 1
|
31
|
+
width = img.width || 1
|
32
|
+
|
33
|
+
palette = []
|
34
|
+
palette_map = {}
|
35
|
+
idx_image = Array.new(height * width)
|
36
|
+
buf = img.buffer
|
37
|
+
idx = 0
|
38
|
+
step = buf.pixel_bytes
|
39
|
+
|
40
|
+
height.times do |y|
|
41
|
+
row = y * width
|
42
|
+
width.times do |x|
|
43
|
+
color = buf.get_index(idx)
|
44
|
+
key = (color[0] || 255) |
|
45
|
+
((color[1] || 255) << 8) |
|
46
|
+
((color[2] || 255) << 16) |
|
47
|
+
((color[3] || 255) << 24)
|
48
|
+
pal_idx = palette_map[key]
|
49
|
+
unless pal_idx
|
50
|
+
pal_idx = palette.length
|
51
|
+
palette_map[key] = pal_idx
|
52
|
+
palette << color
|
53
|
+
end
|
54
|
+
idx_image[row + x] = pal_idx
|
55
|
+
idx += step
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
out = "\ePq".dup
|
60
|
+
palette.each_with_index do |c, idx|
|
61
|
+
out << format("#%d;2;%d;%d;%d", idx, c.r * 100 / 255, c.g * 100 / 255, c.b * 100 / 255)
|
62
|
+
end
|
63
|
+
|
64
|
+
(0...height).step(6) do |y|
|
65
|
+
palette.each_index do |idx|
|
66
|
+
out << "##{idx}"
|
67
|
+
run_char = nil
|
68
|
+
run_len = 0
|
69
|
+
x = 0
|
70
|
+
while x < width
|
71
|
+
bits = 0
|
72
|
+
yy = y
|
73
|
+
i = 0
|
74
|
+
while i < 6
|
75
|
+
if yy < height && idx_image[yy * width + x] == idx
|
76
|
+
bits |= 1 << i
|
77
|
+
end
|
78
|
+
yy += 1
|
79
|
+
i += 1
|
80
|
+
end
|
81
|
+
char = CHAR_MAP[bits]
|
82
|
+
if char == run_char
|
83
|
+
run_len += 1
|
84
|
+
else
|
85
|
+
out << "!#{run_len}" << run_char if run_len > 1
|
86
|
+
out << run_char if run_len == 1
|
87
|
+
run_char = char
|
88
|
+
run_len = 1
|
89
|
+
end
|
90
|
+
x += 1
|
91
|
+
end
|
92
|
+
out << "!#{run_len}" << run_char if run_len > 1
|
93
|
+
out << run_char if run_len == 1
|
94
|
+
out << "$"
|
95
|
+
end
|
96
|
+
out << "-"
|
97
|
+
end
|
98
|
+
|
99
|
+
out << "\e\\"
|
100
|
+
out
|
101
|
+
end
|
102
|
+
|
103
|
+
def encode_io(format, image, io)
|
104
|
+
io << encode(format, image)
|
105
|
+
end
|
106
|
+
|
107
|
+
def decode(*)
|
108
|
+
raise UnsupportedFormatError, "decode not supported for sixel"
|
109
|
+
end
|
110
|
+
|
111
|
+
def decode_io(*)
|
112
|
+
raise UnsupportedFormatError, "decode not supported for sixel"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
module Codec
|
5
|
+
class UnsupportedFormatError < Error; end
|
6
|
+
|
7
|
+
@encoders = []
|
8
|
+
@decoders = []
|
9
|
+
|
10
|
+
class << self
|
11
|
+
attr_reader :encoders, :decoders
|
12
|
+
|
13
|
+
def register_encoder(codec_const, *formats)
|
14
|
+
encoders << { codec: codec_const, formats: formats.map { |f| f.to_s.downcase } }
|
15
|
+
end
|
16
|
+
|
17
|
+
def register_decoder(codec_const, *formats)
|
18
|
+
decoders << { codec: codec_const, formats: formats.map { |f| f.to_s.downcase } }
|
19
|
+
end
|
20
|
+
|
21
|
+
def register_codec(codec_const, *formats)
|
22
|
+
register_encoder(codec_const, *formats)
|
23
|
+
register_decoder(codec_const, *formats)
|
24
|
+
end
|
25
|
+
|
26
|
+
def supported?(format)
|
27
|
+
fmt = format.to_s.downcase
|
28
|
+
encoders.any? { |r| r[:formats].include?(fmt) && codec_supported?(r[:codec], fmt) } ||
|
29
|
+
decoders.any? { |r| r[:formats].include?(fmt) && codec_supported?(r[:codec], fmt) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def encode(format, image, codec: nil, **kwargs)
|
33
|
+
codec = find_codec(encoders, format, codec)
|
34
|
+
codec.encode(format, image, **kwargs)
|
35
|
+
end
|
36
|
+
|
37
|
+
def decode(format, data, codec: nil, **kwargs)
|
38
|
+
codec = find_codec(decoders, format, codec)
|
39
|
+
codec.decode(format, data, **kwargs)
|
40
|
+
end
|
41
|
+
|
42
|
+
def encode_io(format, image, io, codec: nil, **kwargs)
|
43
|
+
codec = find_codec(encoders, format, codec)
|
44
|
+
if codec.respond_to?(:encode_io)
|
45
|
+
codec.encode_io(format, image, io, **kwargs)
|
46
|
+
else
|
47
|
+
io << codec.encode(format, image, **kwargs)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def decode_io(format, io, codec: nil, **kwargs)
|
52
|
+
codec = find_codec(decoders, format, codec)
|
53
|
+
if codec.respond_to?(:decode_io)
|
54
|
+
codec.decode_io(format, io, **kwargs)
|
55
|
+
else
|
56
|
+
codec.decode(format, io.read, **kwargs)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def detect(data)
|
61
|
+
Magic.detect(data)
|
62
|
+
end
|
63
|
+
|
64
|
+
def detect_io(io)
|
65
|
+
Magic.detect_io(io).first
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def find_codec(list, format, preferred = nil)
|
71
|
+
fmt = format.to_s.downcase
|
72
|
+
if preferred
|
73
|
+
r = list.find { |e| e[:formats].include?(fmt) && e[:codec].to_s == preferred.to_s }
|
74
|
+
raise UnsupportedFormatError, "unsupported format #{format}" unless r
|
75
|
+
|
76
|
+
codec = const_get(r[:codec])
|
77
|
+
unless !codec.respond_to?(:supported?) || codec.supported?(fmt.to_sym)
|
78
|
+
raise UnsupportedFormatError, "unsupported format #{format}"
|
79
|
+
end
|
80
|
+
|
81
|
+
return codec
|
82
|
+
end
|
83
|
+
|
84
|
+
list.each do |r|
|
85
|
+
next unless r[:formats].include?(fmt)
|
86
|
+
|
87
|
+
codec = const_get(r[:codec])
|
88
|
+
next if codec.respond_to?(:supported?) && !codec.supported?(fmt.to_sym)
|
89
|
+
|
90
|
+
return codec
|
91
|
+
end
|
92
|
+
raise UnsupportedFormatError, "unsupported format #{format}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def codec_supported?(codec_const, fmt)
|
96
|
+
codec = const_get(codec_const)
|
97
|
+
!codec.respond_to?(:supported?) || codec.supported?(fmt.to_sym)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
autoload :Guard, "image_util/codec/_guard"
|
102
|
+
|
103
|
+
autoload :Libpng, "image_util/codec/libpng"
|
104
|
+
autoload :Libturbojpeg, "image_util/codec/libturbojpeg"
|
105
|
+
autoload :Pam, "image_util/codec/pam"
|
106
|
+
autoload :Libsixel, "image_util/codec/libsixel"
|
107
|
+
autoload :ImageMagick, "image_util/codec/image_magick"
|
108
|
+
autoload :RubySixel, "image_util/codec/ruby_sixel"
|
109
|
+
|
110
|
+
register_codec :Pam, :pam
|
111
|
+
register_codec :Libpng, :png
|
112
|
+
register_codec :Libturbojpeg, :jpeg
|
113
|
+
register_encoder :Libsixel, :sixel
|
114
|
+
register_encoder :ImageMagick, :sixel
|
115
|
+
register_encoder :RubySixel, :sixel
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
class Color < Array
|
5
|
+
def initialize(*args)
|
6
|
+
super(args)
|
7
|
+
end
|
8
|
+
|
9
|
+
def r = self[0]
|
10
|
+
def g = self[1]
|
11
|
+
def b = self[2]
|
12
|
+
def a = self[3] || 255
|
13
|
+
|
14
|
+
def r=(val); self[0] = val; end
|
15
|
+
def g=(val); self[1] = val; end
|
16
|
+
def b=(val); self[2] = val; end
|
17
|
+
def a=(val); self[3] = val; end
|
18
|
+
|
19
|
+
def self.component_from_number(number)
|
20
|
+
case number
|
21
|
+
when nil
|
22
|
+
number
|
23
|
+
when Integer
|
24
|
+
number.clamp(0, 255)
|
25
|
+
when Float
|
26
|
+
(number * 255).clamp(0, 255)
|
27
|
+
else
|
28
|
+
raise ArgumentError, "wrong type passed as component (passed: #{number})"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.from_buffer(buffer, color_bits)
|
33
|
+
buffer.map do |i|
|
34
|
+
case color_bits
|
35
|
+
when 8
|
36
|
+
i
|
37
|
+
else
|
38
|
+
i.to_f / 2**(color_bits - 8)
|
39
|
+
end
|
40
|
+
end.then { |val| new(*val) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_buffer(color_bits, color_length)
|
44
|
+
map do |i|
|
45
|
+
case color_bits
|
46
|
+
when 8
|
47
|
+
i
|
48
|
+
else
|
49
|
+
(i.to_f * 2**(color_bits - 8)).to_i
|
50
|
+
end
|
51
|
+
end + [255] * (color_length - length)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.from(value)
|
55
|
+
case value
|
56
|
+
when Color
|
57
|
+
value
|
58
|
+
when Array
|
59
|
+
value.map do |i|
|
60
|
+
component_from_number(i)
|
61
|
+
rescue ArgumentError
|
62
|
+
raise ArgumentError, "wrong type passed as array index (passed: #{value.inspect})"
|
63
|
+
end.then { |val| new(*val) }
|
64
|
+
when String
|
65
|
+
case value
|
66
|
+
when /\A#(\h)(\h)(\h)\z/
|
67
|
+
new($1.to_i(16) * 0x11, $2.to_i(16) * 0x11, $3.to_i(16) * 0x11)
|
68
|
+
when /\A#(\h{2})(\h{2})(\h{2})\z/
|
69
|
+
new($1.to_i(16), $2.to_i(16), $3.to_i(16))
|
70
|
+
when /\A#(\h{2})(\h{2})(\h{2})(\h{2})\z/
|
71
|
+
new($1.to_i(16), $2.to_i(16), $3.to_i(16), $4.to_i(16))
|
72
|
+
when "black" then new(0, 0, 0)
|
73
|
+
when "white" then new(255, 255, 255)
|
74
|
+
when "red" then new(255, 0, 0)
|
75
|
+
when "lime" then new(0, 255, 0)
|
76
|
+
when "blue" then new(0, 0, 255)
|
77
|
+
else
|
78
|
+
raise ArgumentError, "wrong String passed as color (passed: #{value.inspect})"
|
79
|
+
end
|
80
|
+
when Symbol
|
81
|
+
from(value.to_s)
|
82
|
+
when Integer, Float, nil
|
83
|
+
new(*[component_from_number(value)] * 3)
|
84
|
+
else
|
85
|
+
raise ArgumentError, "wrong type passed as color (passed: #{value.inspect})"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.[](*value)
|
90
|
+
value = value.first if value.is_a?(Array) && value.length == 1
|
91
|
+
from(value)
|
92
|
+
end
|
93
|
+
|
94
|
+
def inspect
|
95
|
+
if a != 255
|
96
|
+
"#%02x%02x%02x%02x" % [r, g, b, a]
|
97
|
+
else
|
98
|
+
"#%02x%02x%02x" % [r, g, b]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def pretty_print(q)
|
103
|
+
q.text inspect
|
104
|
+
end
|
105
|
+
|
106
|
+
def ==(other)
|
107
|
+
other = begin
|
108
|
+
Color.from(other)
|
109
|
+
rescue StandardError
|
110
|
+
nil
|
111
|
+
end
|
112
|
+
return false unless other.is_a?(Color)
|
113
|
+
|
114
|
+
self_rgb = self[0, 3]
|
115
|
+
other_rgb = other[0, 3]
|
116
|
+
return false unless self_rgb == other_rgb
|
117
|
+
|
118
|
+
(self[3] || 255) == (other[3] || 255)
|
119
|
+
end
|
120
|
+
|
121
|
+
alias eql? ==
|
122
|
+
|
123
|
+
# Overlays another color on top of this one taking the alpha
|
124
|
+
# channel of both colors into account.
|
125
|
+
def +(other)
|
126
|
+
other = Color.from(other)
|
127
|
+
|
128
|
+
base_a = a.to_f / 255
|
129
|
+
over_a = other.a.to_f / 255
|
130
|
+
|
131
|
+
out_a = over_a + base_a * (1 - over_a)
|
132
|
+
|
133
|
+
return Color.new(0, 0, 0, 0) if out_a.zero?
|
134
|
+
|
135
|
+
out_r = (other.r * over_a + r * base_a * (1 - over_a)) / out_a
|
136
|
+
out_g = (other.g * over_a + g * base_a * (1 - over_a)) / out_a
|
137
|
+
out_b = (other.b * over_a + b * base_a * (1 - over_a)) / out_a
|
138
|
+
|
139
|
+
Color.new(out_r, out_g, out_b, out_a * 255)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Multiplies the alpha channel by the given factor and returns a new color.
|
143
|
+
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))
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
module Filter
|
5
|
+
module Mixin
|
6
|
+
def define_immutable_version(*names)
|
7
|
+
names.each do |name|
|
8
|
+
define_method(name) do |*args, **kwargs, &block|
|
9
|
+
dup.tap { |obj| obj.public_send("#{name}!", *args, **kwargs, &block) }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
module Filter
|
5
|
+
module Background
|
6
|
+
def background(bgcolor)
|
7
|
+
return self if color_length == 3
|
8
|
+
|
9
|
+
unless color_length == 4
|
10
|
+
raise ArgumentError, "background only supported on RGB or RGBA images"
|
11
|
+
end
|
12
|
+
|
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|
|
16
|
+
over = bg + self[*loc]
|
17
|
+
Color.new(over.r, over.g, over.b)
|
18
|
+
end
|
19
|
+
img
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|