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,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
module Filter
|
5
|
+
module Dither
|
6
|
+
extend ImageUtil::Filter::Mixin
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def dither_distance_sq(c1, c2)
|
11
|
+
len = [c1.length, c2.length].max
|
12
|
+
|
13
|
+
case len
|
14
|
+
when 1
|
15
|
+
d = (c1[0] || 255) - (c2[0] || 255)
|
16
|
+
d * d
|
17
|
+
when 2
|
18
|
+
d0 = (c1[0] || 255) - (c2[0] || 255)
|
19
|
+
d1 = (c1[1] || 255) - (c2[1] || 255)
|
20
|
+
d0 * d0 + d1 * d1
|
21
|
+
when 3
|
22
|
+
d0 = (c1[0] || 255) - (c2[0] || 255)
|
23
|
+
d1 = (c1[1] || 255) - (c2[1] || 255)
|
24
|
+
d2 = (c1[2] || 255) - (c2[2] || 255)
|
25
|
+
d0 * d0 + d1 * d1 + d2 * d2
|
26
|
+
when 4
|
27
|
+
d0 = (c1[0] || 255) - (c2[0] || 255)
|
28
|
+
d1 = (c1[1] || 255) - (c2[1] || 255)
|
29
|
+
d2 = (c1[2] || 255) - (c2[2] || 255)
|
30
|
+
d3 = (c1[3] || 255) - (c2[3] || 255)
|
31
|
+
d0 * d0 + d1 * d1 + d2 * d2 + d3 * d3
|
32
|
+
else
|
33
|
+
sum = 0
|
34
|
+
len.times do |i|
|
35
|
+
d = (c1[i] || 255) - (c2[i] || 255)
|
36
|
+
sum += d * d
|
37
|
+
end
|
38
|
+
sum
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
public
|
43
|
+
|
44
|
+
def dither!(count)
|
45
|
+
palette = histogram.sort_by { |_, v| -v - rand }.first(count).map(&:first)
|
46
|
+
|
47
|
+
cache = {}
|
48
|
+
|
49
|
+
nearest = lambda do |color|
|
50
|
+
key = (color[0] || 255) |
|
51
|
+
((color[1] || 255) << 8) |
|
52
|
+
((color[2] || 255) << 16) |
|
53
|
+
((color[3] || 255) << 24)
|
54
|
+
cache[key] ||= begin
|
55
|
+
best = palette.first
|
56
|
+
best_dist = dither_distance_sq(color, best)
|
57
|
+
idx = 1
|
58
|
+
while idx < palette.length
|
59
|
+
c = palette[idx]
|
60
|
+
dist = dither_distance_sq(color, c)
|
61
|
+
if dist < best_dist
|
62
|
+
best = c
|
63
|
+
best_dist = dist
|
64
|
+
end
|
65
|
+
idx += 1
|
66
|
+
end
|
67
|
+
best
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
if dimensions.length == 2
|
72
|
+
w = width
|
73
|
+
h = height
|
74
|
+
buf = buffer
|
75
|
+
idx = 0
|
76
|
+
step = buf.pixel_bytes
|
77
|
+
h.times do
|
78
|
+
w.times do
|
79
|
+
color = buf.get_index(idx)
|
80
|
+
buf.set_index(idx, nearest.call(color))
|
81
|
+
idx += step
|
82
|
+
end
|
83
|
+
end
|
84
|
+
else
|
85
|
+
set_each_pixel_by_location do |loc|
|
86
|
+
color = self[*loc]
|
87
|
+
nearest.call(color)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
self
|
91
|
+
end
|
92
|
+
|
93
|
+
define_immutable_version :dither
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
module Filter
|
5
|
+
module Draw
|
6
|
+
extend ImageUtil::Filter::Mixin
|
7
|
+
|
8
|
+
# rubocop:disable Metrics/ParameterLists
|
9
|
+
def draw_function!(
|
10
|
+
color = Color[:black],
|
11
|
+
limit = nil,
|
12
|
+
axis:,
|
13
|
+
draw_axis: nil,
|
14
|
+
view: View::Interpolated,
|
15
|
+
plane: [0,0]
|
16
|
+
)
|
17
|
+
fp = self.view(view)
|
18
|
+
|
19
|
+
axis = axis_to_number(axis)
|
20
|
+
draw_axis ||= case axis
|
21
|
+
when 0 then 1
|
22
|
+
when 1 then 0
|
23
|
+
end
|
24
|
+
draw_axis = axis_to_number(draw_axis)
|
25
|
+
|
26
|
+
limit ||= (0..)
|
27
|
+
limit = Range.new(limit.begin, dimensions[axis]-1, false) if limit.end == nil
|
28
|
+
|
29
|
+
limit.each do |axispoint|
|
30
|
+
draw = yield(axispoint)
|
31
|
+
|
32
|
+
loc = plane.dup
|
33
|
+
loc[axis] = axispoint
|
34
|
+
loc[draw_axis] = draw
|
35
|
+
|
36
|
+
fp[*loc] = color
|
37
|
+
end
|
38
|
+
|
39
|
+
self
|
40
|
+
end
|
41
|
+
# rubocop:enable Metrics/ParameterLists
|
42
|
+
|
43
|
+
def draw_line!(begin_loc, end_loc, color = Color[:black], view: View::Interpolated)
|
44
|
+
begin_x, begin_y = begin_loc
|
45
|
+
end_x, end_y = end_loc
|
46
|
+
|
47
|
+
dist_x = (end_x - begin_x).abs
|
48
|
+
dist_y = (end_y - begin_y).abs
|
49
|
+
|
50
|
+
if dist_x < dist_y
|
51
|
+
begin_x, end_x, begin_y, end_y = end_x, begin_x, end_y, begin_y if begin_y > end_y
|
52
|
+
a = (end_x - begin_x).to_f / (end_y - begin_y)
|
53
|
+
draw_function!(color, begin_y..end_y, axis: :y, view: view) do |y|
|
54
|
+
begin_x + (y - begin_y) * a
|
55
|
+
end
|
56
|
+
else
|
57
|
+
begin_x, end_x, begin_y, end_y = end_x, begin_x, end_y, begin_y if begin_x > end_x
|
58
|
+
a = (end_y - begin_y).to_f / (end_x - begin_x)
|
59
|
+
draw_function!(color, begin_x..end_x, axis: :x, view: view) do |x|
|
60
|
+
begin_y + (x - begin_x) * a
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
define_immutable_version :draw_function, :draw_line
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def axis_to_number(axis)
|
70
|
+
axis = 0 if axis == :x
|
71
|
+
axis = 1 if axis == :y
|
72
|
+
axis = 2 if axis == :z
|
73
|
+
axis
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
module Filter
|
5
|
+
module Paste
|
6
|
+
extend ImageUtil::Filter::Mixin
|
7
|
+
|
8
|
+
def paste!(image, *location, respect_alpha: false)
|
9
|
+
raise TypeError, "image must be an Image" unless image.is_a?(Image)
|
10
|
+
|
11
|
+
if !respect_alpha &&
|
12
|
+
image.dimensions.length == 1 &&
|
13
|
+
image.color_bits == color_bits &&
|
14
|
+
image.color_length == color_length &&
|
15
|
+
buffer.respond_to?(:copy_1d)
|
16
|
+
loc = location.map(&:to_i)
|
17
|
+
begin
|
18
|
+
check_bounds!(loc)
|
19
|
+
rescue IndexError
|
20
|
+
return self
|
21
|
+
end
|
22
|
+
|
23
|
+
if loc.first + image.length <= width
|
24
|
+
buffer.copy_1d(image.buffer, *loc)
|
25
|
+
return self
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
last_dim = image.dimensions.length - 1
|
30
|
+
|
31
|
+
image.each_with_index do |val, idx|
|
32
|
+
new_loc = location.dup
|
33
|
+
new_loc[last_dim] += idx
|
34
|
+
|
35
|
+
begin
|
36
|
+
if val.is_a?(Image)
|
37
|
+
paste!(val, *new_loc, respect_alpha: respect_alpha)
|
38
|
+
elsif respect_alpha
|
39
|
+
self[*new_loc] += val
|
40
|
+
else
|
41
|
+
self[*new_loc] = val
|
42
|
+
end
|
43
|
+
rescue IndexError
|
44
|
+
# do nothing, image overlaps
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
define_immutable_version :paste
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
module Filter
|
5
|
+
module Resize
|
6
|
+
def resize(*new_dimensions, view: View::Interpolated)
|
7
|
+
src = self.view(view)
|
8
|
+
|
9
|
+
factors = new_dimensions.zip(dimensions).map do |new_dim, old_dim|
|
10
|
+
new_dim == 1 ? 0.0 : (old_dim - 1).to_f / (new_dim - 1)
|
11
|
+
end
|
12
|
+
|
13
|
+
Image.new(*new_dimensions, color_bits: color_bits, color_length: color_length) do |loc|
|
14
|
+
src_loc = loc.zip(factors).map { |coord, factor| coord * factor }
|
15
|
+
src[*src_loc]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
module Filter
|
5
|
+
autoload :Dither, "image_util/filter/dither"
|
6
|
+
autoload :Background, "image_util/filter/background"
|
7
|
+
autoload :Paste, "image_util/filter/paste"
|
8
|
+
autoload :Draw, "image_util/filter/draw"
|
9
|
+
autoload :Resize, "image_util/filter/resize"
|
10
|
+
|
11
|
+
autoload :Mixin, "image_util/filter/_mixin"
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Silence a warning.
|
4
|
+
Warning[:experimental] = false
|
5
|
+
|
6
|
+
module ImageUtil
|
7
|
+
class Image
|
8
|
+
class Buffer
|
9
|
+
def initialize(dimensions, color_bits, color_length, buffer = nil)
|
10
|
+
@color_type = case color_bits
|
11
|
+
when 8
|
12
|
+
:U8
|
13
|
+
when 16
|
14
|
+
:u16
|
15
|
+
when 32
|
16
|
+
:u32
|
17
|
+
else
|
18
|
+
raise ArgumentError, "wrong color bits provided: #{color_bits.inspect}"
|
19
|
+
end
|
20
|
+
|
21
|
+
@dimensions = dimensions.freeze
|
22
|
+
@color_bits = color_bits
|
23
|
+
@color_bytes = color_bits / 8
|
24
|
+
@color_length = color_length
|
25
|
+
|
26
|
+
@buffer_size = @dimensions.reduce(&:*)
|
27
|
+
@buffer_size *= @color_length
|
28
|
+
@buffer_size *= @color_bytes
|
29
|
+
|
30
|
+
@io_buffer_types = ([@color_type]*@color_length).freeze
|
31
|
+
|
32
|
+
@buffer = buffer || IO::Buffer.new(@buffer_size)
|
33
|
+
|
34
|
+
apply_singleton_optimizations!
|
35
|
+
|
36
|
+
freeze
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :dimensions, :color_bits, :color_bytes, :color_length
|
40
|
+
|
41
|
+
def offset_of(*location)
|
42
|
+
location.length == @dimensions.length or raise ArgumentError, "wrong number of dimensions"
|
43
|
+
|
44
|
+
offset = 0
|
45
|
+
location.reverse.zip(@dimensions.reverse) do |i,max|
|
46
|
+
offset *= max
|
47
|
+
offset += i
|
48
|
+
end
|
49
|
+
|
50
|
+
offset * pixel_bytes
|
51
|
+
end
|
52
|
+
|
53
|
+
def pixel_bytes = @color_length * @color_bytes
|
54
|
+
|
55
|
+
def initialize_copy(_other)
|
56
|
+
@buffer = @buffer.dup
|
57
|
+
end
|
58
|
+
|
59
|
+
def get(location)
|
60
|
+
get_index(offset_of(*location))
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_index(index)
|
64
|
+
value = @buffer.get_values(@io_buffer_types, index)
|
65
|
+
Color.from_buffer(value, @color_bits)
|
66
|
+
end
|
67
|
+
|
68
|
+
def set(location, value)
|
69
|
+
set_index(offset_of(*location), value)
|
70
|
+
end
|
71
|
+
|
72
|
+
def set_index(index, value)
|
73
|
+
value = Color.from(value).to_buffer(@color_bits, @color_length)
|
74
|
+
@buffer.set_values(@io_buffer_types, index, value)
|
75
|
+
end
|
76
|
+
|
77
|
+
def last_dimension(i)
|
78
|
+
dimensions_without_last = dimensions[0..-2]
|
79
|
+
remaining_dimensions = dimensions_without_last.length
|
80
|
+
o0 = offset_of(*[0] * remaining_dimensions, i)
|
81
|
+
o1 = offset_of(*[0] * remaining_dimensions, i + 1)
|
82
|
+
Buffer.new(
|
83
|
+
dimensions_without_last,
|
84
|
+
@color_bits,
|
85
|
+
@color_length,
|
86
|
+
@buffer.slice(o0, o1 - o0)
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
def last_dimension_split
|
91
|
+
dimensions.last.times.map do |i|
|
92
|
+
last_dimension(i)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def get_string = @buffer.get_string
|
97
|
+
|
98
|
+
def io_buffer = @buffer
|
99
|
+
|
100
|
+
def copy_1d(other, *location)
|
101
|
+
index = offset_of(*location)
|
102
|
+
length = [other.width, width - location.first].min
|
103
|
+
return if length <= 0
|
104
|
+
|
105
|
+
@buffer.copy(other.io_buffer, index, length * pixel_bytes)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Optimizations for most common usecases:
|
109
|
+
def apply_singleton_optimizations!
|
110
|
+
# rubocop:disable Style/GuardClause
|
111
|
+
if OPT_OFFSET_OF.key?(@dimensions.length)
|
112
|
+
singleton_class.define_method(:offset_of, &OPT_OFFSET_OF[@dimensions.length])
|
113
|
+
end
|
114
|
+
|
115
|
+
if OPT_GET_INDEX.key?(@color_bits)
|
116
|
+
singleton_class.define_method(:get_index, &OPT_GET_INDEX[@color_bits])
|
117
|
+
end
|
118
|
+
# rubocop:enable Style/GuardClause
|
119
|
+
end
|
120
|
+
|
121
|
+
def width = dimensions[0]
|
122
|
+
|
123
|
+
OPT_OFFSET_OF = {
|
124
|
+
1 => ->(x) { x * pixel_bytes },
|
125
|
+
2 => ->(x,y) { (y * width + x) * pixel_bytes }
|
126
|
+
}.freeze
|
127
|
+
|
128
|
+
OPT_GET_INDEX = {
|
129
|
+
8 => ->(index) { Color.new(*@buffer.get_values(@io_buffer_types, index)) }
|
130
|
+
}.freeze
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,242 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
class Image
|
5
|
+
autoload :Buffer, "image_util/image/buffer"
|
6
|
+
autoload :PixelView, "image_util/image/pixel_view"
|
7
|
+
|
8
|
+
Util.irb_fixup
|
9
|
+
|
10
|
+
ALL = nil..nil
|
11
|
+
|
12
|
+
def initialize(*dimensions, color_bits: 8, color_length: 4, &block)
|
13
|
+
@buf = Buffer.new(dimensions, color_bits, color_length)
|
14
|
+
|
15
|
+
set_each_pixel_by_location(&block) if block_given?
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize_from_buffer(buffer)
|
19
|
+
@buf = buffer
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize_copy(_other)
|
23
|
+
@buf = @buf.dup
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.from_buffer(...)
|
27
|
+
allocate.tap { |i| i.initialize_from_buffer(...) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.from_string(data, format = nil, codec: nil, **kwargs)
|
31
|
+
format ||= Codec.detect(data)
|
32
|
+
raise ArgumentError, "could not detect format" unless format
|
33
|
+
|
34
|
+
Codec.decode(format, data, codec: codec, **kwargs)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.from_file(path_or_io, format = nil, codec: nil, **kwargs)
|
38
|
+
if format
|
39
|
+
if path_or_io.respond_to?(:read)
|
40
|
+
path_or_io.binmode if path_or_io.respond_to?(:binmode)
|
41
|
+
Codec.decode_io(format, path_or_io, codec: codec, **kwargs)
|
42
|
+
else
|
43
|
+
File.open(path_or_io, "rb") do |io|
|
44
|
+
Codec.decode_io(format, io, codec: codec, **kwargs)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
elsif path_or_io.respond_to?(:read)
|
48
|
+
path_or_io.binmode if path_or_io.respond_to?(:binmode)
|
49
|
+
fmt, io = Magic.detect_io(path_or_io)
|
50
|
+
raise ArgumentError, "could not detect format" unless fmt
|
51
|
+
|
52
|
+
Codec.decode_io(fmt, io, codec: codec, **kwargs)
|
53
|
+
else
|
54
|
+
File.open(path_or_io, "rb") do |io|
|
55
|
+
fmt, io = Magic.detect_io(io)
|
56
|
+
raise ArgumentError, "could not detect format" unless fmt
|
57
|
+
|
58
|
+
Codec.decode_io(fmt, io, codec: codec, **kwargs)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def buffer = @buf
|
64
|
+
def dimensions = @buf.dimensions
|
65
|
+
def width = dimensions[0]
|
66
|
+
def height = dimensions[1]
|
67
|
+
def length = dimensions[2]
|
68
|
+
def color_bits = @buf.color_bits
|
69
|
+
def color_length = @buf.color_length
|
70
|
+
def pixel_bytes = @buf.pixel_bytes
|
71
|
+
|
72
|
+
def location_expand(location)
|
73
|
+
counts = []
|
74
|
+
|
75
|
+
location = location.reverse.map.with_index do |i,idx|
|
76
|
+
if i.is_a?(Range) && i.begin == nil
|
77
|
+
i = Range.new(0, i.end, i.exclude_end?)
|
78
|
+
end
|
79
|
+
if i.is_a?(Range) && i.end == nil
|
80
|
+
i = Range.new(i.begin, dimensions[-idx-1]-1, false)
|
81
|
+
end
|
82
|
+
|
83
|
+
i = i.to_i if i.is_a? Float
|
84
|
+
|
85
|
+
i = Array(i)
|
86
|
+
|
87
|
+
counts << i.count
|
88
|
+
|
89
|
+
i
|
90
|
+
end
|
91
|
+
|
92
|
+
[counts.reverse, location.shift.product(*location).map(&:reverse)]
|
93
|
+
end
|
94
|
+
|
95
|
+
def check_bounds!(location)
|
96
|
+
location.each_with_index do |i,idx|
|
97
|
+
if i < 0 || i >= dimensions[idx]
|
98
|
+
raise IndexError, "out of bounds image access (#{location.inspect} exceeds #{dimensions.inspect})"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def [](*location)
|
104
|
+
if location.all?(Numeric)
|
105
|
+
location = location.map(&:to_i)
|
106
|
+
check_bounds!(location)
|
107
|
+
@buf.get(location)
|
108
|
+
else
|
109
|
+
new_dimensions, locations = location_expand(location)
|
110
|
+
new_image = Image.new(*new_dimensions,
|
111
|
+
color_bits: color_bits,
|
112
|
+
color_length: color_length)
|
113
|
+
|
114
|
+
locations.each_with_index do |i, idx|
|
115
|
+
new_image.buffer.set_index(idx * @buf.pixel_bytes, @buf.get(i))
|
116
|
+
end
|
117
|
+
|
118
|
+
new_image
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def full_image_location = [ALL]*dimensions.length
|
123
|
+
|
124
|
+
def all=(value)
|
125
|
+
self[*full_image_location] = value
|
126
|
+
end
|
127
|
+
|
128
|
+
def []=(*location, value)
|
129
|
+
if location.all?(Numeric)
|
130
|
+
case value
|
131
|
+
when Image
|
132
|
+
paste!(value, *location)
|
133
|
+
else
|
134
|
+
location = location.map(&:to_i)
|
135
|
+
check_bounds!(location)
|
136
|
+
@buf.set(location, value)
|
137
|
+
end
|
138
|
+
else
|
139
|
+
_, locations = location_expand(location)
|
140
|
+
locations.each do |loc|
|
141
|
+
self[*loc] = value
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def to_a
|
147
|
+
if dimensions.length == 1
|
148
|
+
dimensions.first.times.map { |i| self[i] }
|
149
|
+
else
|
150
|
+
@buf.last_dimension_split.map { |i| Image.from_buffer(i) }
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def deep_to_a
|
155
|
+
to_a.map do |i|
|
156
|
+
case i
|
157
|
+
when Image
|
158
|
+
i.deep_to_a
|
159
|
+
else
|
160
|
+
i
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def each(...)
|
166
|
+
to_a.each(...)
|
167
|
+
end
|
168
|
+
|
169
|
+
include Enumerable
|
170
|
+
include Filter::Dither
|
171
|
+
include Filter::Background
|
172
|
+
include Filter::Paste
|
173
|
+
include Filter::Draw
|
174
|
+
include Filter::Resize
|
175
|
+
include Statistic::Color
|
176
|
+
|
177
|
+
def length = dimensions.last
|
178
|
+
|
179
|
+
def to_pam(fill_to: nil)
|
180
|
+
Codec.encode(:pam, self, fill_to: fill_to)
|
181
|
+
end
|
182
|
+
|
183
|
+
def to_string(format, codec: nil, **kwargs)
|
184
|
+
Codec.encode(format, self, codec: codec, **kwargs)
|
185
|
+
end
|
186
|
+
|
187
|
+
def to_file(path_or_io, format, codec: nil, **kwargs)
|
188
|
+
if path_or_io.respond_to?(:write)
|
189
|
+
path_or_io.binmode if path_or_io.respond_to?(:binmode)
|
190
|
+
Codec.encode_io(format, self, path_or_io, codec: codec, **kwargs)
|
191
|
+
else
|
192
|
+
File.open(path_or_io, "wb") do |io|
|
193
|
+
Codec.encode_io(format, self, io, codec: codec, **kwargs)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def to_sixel
|
199
|
+
Codec.encode(:sixel, self)
|
200
|
+
end
|
201
|
+
|
202
|
+
alias inspect to_sixel
|
203
|
+
|
204
|
+
def pretty_print(p)
|
205
|
+
p.flush
|
206
|
+
p.output << to_sixel
|
207
|
+
p.text("", 0)
|
208
|
+
end
|
209
|
+
|
210
|
+
def pixel_count(locations) = location_expand(locations).first.reduce(:*)
|
211
|
+
|
212
|
+
def each_pixel_location(locations = full_image_location, ...)
|
213
|
+
location_expand(locations).last.each(...)
|
214
|
+
end
|
215
|
+
|
216
|
+
def each_pixel(locations = full_image_location)
|
217
|
+
return enum_for(:each_pixel) { pixel_count(locations) } unless block_given?
|
218
|
+
|
219
|
+
each_pixel_location(locations) do |location|
|
220
|
+
yield self[*location]
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def set_each_pixel_by_location(locations = full_image_location)
|
225
|
+
return enum_for(:set_each_pixel_by_location) { pixel_count(locations) } unless block_given?
|
226
|
+
|
227
|
+
each_pixel_location(locations) do |location|
|
228
|
+
value = yield location
|
229
|
+
self[*location] = value if value
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def view(obj)
|
234
|
+
if block_given?
|
235
|
+
yield obj.new(self)
|
236
|
+
self
|
237
|
+
else
|
238
|
+
obj.new(self)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "stringio"
|
4
|
+
|
5
|
+
module ImageUtil
|
6
|
+
module Magic
|
7
|
+
MAGIC_NUMBERS = {
|
8
|
+
pam: "P7\n".b,
|
9
|
+
png: "\x89PNG\r\n\x1a\n".b,
|
10
|
+
jpeg: "\xFF\xD8".b
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
BYTES_NEEDED = MAGIC_NUMBERS.values.map(&:bytesize).max
|
14
|
+
|
15
|
+
module_function
|
16
|
+
|
17
|
+
def bytes_needed = BYTES_NEEDED
|
18
|
+
|
19
|
+
def detect(data)
|
20
|
+
return nil unless data
|
21
|
+
|
22
|
+
MAGIC_NUMBERS.each do |fmt, magic|
|
23
|
+
return fmt if data.start_with?(magic)
|
24
|
+
crlf_magic = magic.gsub("\n", "\r\n")
|
25
|
+
return fmt if crlf_magic != magic && data.start_with?(crlf_magic)
|
26
|
+
end
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def detect_io(io)
|
31
|
+
|
32
|
+
pos = io.pos
|
33
|
+
data = io.read(BYTES_NEEDED)
|
34
|
+
io.seek(pos)
|
35
|
+
[detect(data), io]
|
36
|
+
rescue Errno::ESPIPE, IOError
|
37
|
+
data = io.read(BYTES_NEEDED)
|
38
|
+
fmt = detect(data)
|
39
|
+
rest = io.read
|
40
|
+
new_io = StringIO.new((data || "") + (rest || ""))
|
41
|
+
[fmt, new_io]
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|