Module: Retouch::Operations
- Defined in:
- lib/retouch/operations.rb,
lib/retouch/operations/draw.rb,
lib/retouch/operations/color.rb,
lib/retouch/operations/filter.rb,
lib/retouch/operations/multiple.rb,
sig/retouch.rbs
Constant Summary collapse
- WEIGHT_SCALE =
1 << 20
- BLENDS =
%i[normal multiply screen overlay darken lighten add].freeze
Class Method Summary collapse
- .animate(frames, path, fps: nil, delay: nil, loop: true, colors: 256, palette: :global, dither: :floyd_steinberg) ⇒ String
- .append(images, direction: :vertical, gap: 0, background: [0, 0, 0, 0]) ⇒ Object
- .apply(image, name, *arguments, **options) ⇒ Object
- .arrow(image, x1, y1, x2, y2, color = "#ffffff", width: 1, head: 8) ⇒ Object
- .blur(image, sigma) ⇒ Object
- .border(image, width, color = "#000000") ⇒ Object
- .brightness ⇒ Object
- .contrast ⇒ Object
- .crop(image, geometry, gravity: :north_west) ⇒ Object
- .diff(expected, actual, threshold: 0, color: "#ff00ff") ⇒ Hash[Symbol, untyped]
- .extend(image, width, height, color: [0, 0, 0, 0], gravity: :center) ⇒ Object
- .flip(image) ⇒ Object
- .flop(image) ⇒ Object
- .gamma ⇒ Object
- .grayscale ⇒ Object
- .info(image) ⇒ Hash[Symbol, untyped]
- .invert ⇒ Object
- .montage(images, columns: nil, cols: nil, gap: 0, background: [0, 0, 0, 0], labels: nil) ⇒ Object
- .opacity ⇒ Object
- .open_gif(path, frame: :all, max_pixels: 16_384 * 16_384, max_frames: 10_000, max_total_pixels: 100_000_000) ⇒ Object
- .overlay(image, source, x: nil, y: nil, gravity: :center, opacity: 1.0, mode: :normal) ⇒ Object
- .pad(image, amount, color: [0, 0, 0, 0]) ⇒ Object
- .pixelate(image, size) ⇒ Object
- .quantize(image, colors: 256, dither: :none, palette: nil) ⇒ Object
- .rect(image, x, y, width, height, color = "#ffffff", fill: false) ⇒ Object
- .resize(image, geometry, filter: :lanczos3, gravity: :center) ⇒ Object
- .rotate(image, degrees, background: [0, 0, 0, 0]) ⇒ Object
- .saturate ⇒ Object
- .sharpen(image, sigma, amount: 1.0) ⇒ Object
- .spritesheet(images, max_width: 2048, gap: 0, background: [0, 0, 0, 0]) ⇒ [untyped, Array[Hash[Symbol, Integer]]]
- .text(image, string, x: 0, y: 0, at: nil, size: 16, font: nil, color: "#ffffff", background: nil, padding: 4) ⇒ Object
- .thumbnail(image, geometry, **options) ⇒ Object
- .tint(image, color, amount: 1.0) ⇒ Object
- .trim(image, color: nil, fuzz: 0) ⇒ Object
- .watermark(image, source, **options) ⇒ Object
Class Method Details
.animate(frames, path, fps: nil, delay: nil, loop: true, colors: 256, palette: :global, dither: :floyd_steinberg) ⇒ String
133 134 135 136 |
# File 'lib/retouch/operations/multiple.rb', line 133 def animate(frames, path, fps: nil, delay: nil, loop: true, colors: 256, palette: :global, dither: :floyd_steinberg) require_flipbook Flipbook.write(path, validate_images(frames), fps:, delay:, loop:, colors:, palette:, dither:) end |
.append(images, direction: :vertical, gap: 0, background: [0, 0, 0, 0]) ⇒ Object
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# File 'lib/retouch/operations/multiple.rb', line 7 def append(images, direction: :vertical, gap: 0, background: [0, 0, 0, 0]) images = validate_images(images) direction = direction.to_sym gap = Integer(gap) raise ArgumentError, "direction must be vertical or horizontal" unless %i[vertical horizontal].include?(direction) raise ArgumentError, "gap must not be negative" if gap.negative? if direction == :vertical width = images.map(&:width).max height = images.sum(&:height) + (gap * (images.length - 1)) else width = images.sum(&:width) + (gap * (images.length - 1)) height = images.map(&:height).max end output = Tessel::Image.new(width, height, fill: background) x = y = 0 images.each do |image| dx, dy = Gravity.offset(width, height, image.width, image.height, :center) output.blit(image, direction == :vertical ? dx : x, direction == :vertical ? y : dy) direction == :vertical ? y += image.height + gap : x += image.width + gap end output end |
.apply(image, name, *arguments, **options) ⇒ Object
9 10 11 12 13 14 15 16 |
# File 'lib/retouch/operations.rb', line 9 def apply(image, name, *arguments, **) raise TypeError, "image must be a Tessel::Image" unless image.is_a?(Tessel::Image) method = name.to_sym raise ArgumentError, "unknown operation: #{name}" unless respond_to?(method) public_send(method, image, *arguments, **) end |
.arrow(image, x1, y1, x2, y2, color = "#ffffff", width: 1, head: 8) ⇒ Object
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/retouch/operations/draw.rb', line 59 def arrow(image, x1, y1, x2, y2, color = "#ffffff", width: 1, head: 8) x1, y1, x2, y2 = [x1, y1, x2, y2].map { |value| Integer(value) } width = Integer(width) head = Float(head) raise ArgumentError, "arrow width must be positive" unless width.positive? raise ArgumentError, "arrow head length must be non-negative and finite" unless head.finite? && head >= 0 output = image.dup draw_line(output, x1, y1, x2, y2, color, width) distance = Math.hypot(x2 - x1, y2 - y1) if head.positive? && distance.positive? angle = Math.atan2(y2 - y1, x2 - x1) [angle + (0.65 * Math::PI), angle - (0.65 * Math::PI)].each do |side| draw_line(output, x2, y2, (x2 + (Math.cos(side) * head)).round, (y2 + (Math.sin(side) * head)).round, color, width) end end output end |
.blur(image, sigma) ⇒ Object
7 8 9 |
# File 'lib/retouch/operations/filter.rb', line 7 def blur(image, sigma) Kernels::Convolve.gaussian(image, sigma) end |
.border(image, width, color = "#000000") ⇒ Object
117 118 119 120 121 122 |
# File 'lib/retouch/operations.rb', line 117 def border(image, width, color = "#000000") width = Integer(width) raise ArgumentError, "border width must be positive" unless width.positive? pad(image, width, color:) end |
.brightness ⇒ Object
27 |
# File 'sig/retouch.rbs', line 27
def self.brightness: (untyped image, Numeric amount) -> untyped
|
.contrast ⇒ Object
28 |
# File 'sig/retouch.rbs', line 28
def self.contrast: (untyped image, Numeric factor) -> untyped
|
.crop(image, geometry, gravity: :north_west) ⇒ Object
48 49 50 51 52 53 54 55 56 57 |
# File 'lib/retouch/operations.rb', line 48 def crop(image, geometry, gravity: :north_west) geometry = Geometry.parse(geometry) unless geometry.is_a?(Geometry) width, height = geometry.crop_size(image.width, image.height) x, y = if geometry.offset_x.zero? && geometry.offset_y.zero? Gravity.offset(image.width, image.height, width, height, gravity) else [geometry.offset_x, geometry.offset_y] end (image.crop(x, y, width, height), image) end |
.diff(expected, actual, threshold: 0, color: "#ff00ff") ⇒ Hash[Symbol, untyped]
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/retouch/operations/multiple.rb', line 109 def diff(expected, actual, threshold: 0, color: "#ff00ff") raise TypeError, "expected and actual must be Tessel::Image values" unless expected.is_a?(Tessel::Image) && actual.is_a?(Tessel::Image) raise ArgumentError, "images must have the same dimensions" unless expected.width == actual.width && expected.height == actual.height threshold = Float(threshold) raise ArgumentError, "threshold must be between 0 and 255" unless threshold.finite? && threshold.between?(0, 255) marker = Tessel::Color.pack(color).bytes output = Tessel::Image.new(expected.width, expected.height) different = 0 expected.height.times do |y| expected.width.times do |x| left = expected[x, y] right = actual[x, y] if left.zip(right).map { |a, b| (a - b).abs }.max > threshold output[x, y] = marker different += 1 end end end { image: output, pixels: different, total: expected.width * expected.height, rate: expected.width.zero? || expected.height.zero? ? 0.0 : different.to_f / (expected.width * expected.height) } end |
.extend(image, width, height, color: [0, 0, 0, 0], gravity: :center) ⇒ Object
107 108 109 110 111 112 113 114 115 |
# File 'lib/retouch/operations.rb', line 107 def extend(image, width, height, color: [0, 0, 0, 0], gravity: :center) width = Integer(width) height = Integer(height) raise ArgumentError, "canvas dimensions must be positive" unless width.positive? && height.positive? raise ArgumentError, "canvas cannot be smaller than image" if width < image.width || height < image.height x, y = Gravity.offset(width, height, image.width, image.height, gravity) Tessel::Image.new(width, height, fill: color, metadata: image.).tap { |canvas| canvas.blit(image, x, y) } end |
.flip(image) ⇒ Object
59 |
# File 'lib/retouch/operations.rb', line 59 def flip(image) = image.flip_vertical |
.flop(image) ⇒ Object
61 62 63 64 65 66 67 |
# File 'lib/retouch/operations.rb', line 61 def flop(image) output = Tessel::Image.new(image.width, image.height, metadata: image.) image.height.times do |y| image.width.times { |x| output[image.width - x - 1, y] = image[x, y] } end output end |
.gamma ⇒ Object
29 |
# File 'sig/retouch.rbs', line 29
def self.gamma: (untyped image, Numeric value) -> untyped
|
.grayscale ⇒ Object
25 |
# File 'sig/retouch.rbs', line 25
def self.grayscale: (untyped image) -> untyped
|
.info(image) ⇒ Hash[Symbol, untyped]
18 19 20 21 22 23 24 25 26 27 |
# File 'lib/retouch/operations.rb', line 18 def info(image) colors = {} alpha = false image.bytes.bytes.each_slice(4) do |r, g, b, a| colors[[r, g, b, a]] = true alpha ||= a < 255 end { width: image.width, height: image.height, format: image..fetch("format", "RGBA"), colors: colors.length, alpha: alpha, metadata: image..dup } end |
.invert ⇒ Object
26 |
# File 'sig/retouch.rbs', line 26
def self.invert: (untyped image) -> untyped
|
.montage(images, columns: nil, cols: nil, gap: 0, background: [0, 0, 0, 0], labels: nil) ⇒ Object
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/retouch/operations/multiple.rb', line 31 def montage(images, columns: nil, cols: nil, gap: 0, background: [0, 0, 0, 0], labels: nil) images = validate_images(images) columns = Integer(columns || cols || Math.sqrt(images.length).ceil) gap = Integer(gap) raise ArgumentError, "columns must be positive" unless columns.positive? raise ArgumentError, "gap must not be negative" if gap.negative? column_widths = Array.new([images.length, columns].min, 0) row_heights = Array.new((images.length.to_f / columns).ceil, 0) images.each_with_index do |image, index| column = index % columns row = index / columns column_widths[column] = [column_widths[column], image.width].max row_heights[row] = [row_heights[row], image.height + (labels ? 15 : 0)].max end output = Tessel::Image.new(column_widths.sum + (gap * (column_widths.length - 1)), row_heights.sum + (gap * (row_heights.length - 1)), fill: background) row_y = 0 row_heights.each_with_index do |row_height, row| column_x = 0 column_widths.each_with_index do |column_width, column| index = (row * columns) + column break unless images[index] image = images[index] image_y = row_y + ((row_height - image.height - (labels ? 15 : 0)) / 2) output.blit(image, column_x + ((column_width - image.width) / 2), image_y) if labels label = labels == true ? index.to_s : String(labels.fetch(index)) output.fill_rect(column_x, row_y + row_height - 15, column_width, 15, [0, 0, 0, 192], blend: :alpha) output = text(output, label, x: column_x + 3, y: row_y + row_height - 12, size: 9, color: "#ffffff") end column_x += column_width + gap end row_y += row_height + gap end output end |
.opacity ⇒ Object
32 |
# File 'sig/retouch.rbs', line 32
def self.opacity: (untyped image, Numeric amount) -> untyped
|
.open_gif(path, frame: :all, max_pixels: 16_384 * 16_384, max_frames: 10_000, max_total_pixels: 100_000_000) ⇒ Object
138 139 140 141 142 143 144 |
# File 'lib/retouch/operations/multiple.rb', line 138 def open_gif(path, frame: :all, max_pixels: 16_384 * 16_384, max_frames: 10_000, max_total_pixels: 100_000_000) require_flipbook frames = Flipbook.read(path, max_pixels:, max_frames:, max_total_pixels:) return frames if frame == :all frames.fetch(frame == :first ? 0 : Integer(frame)) end |
.overlay(image, source, x: nil, y: nil, gravity: :center, opacity: 1.0, mode: :normal) ⇒ Object
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/retouch/operations/draw.rb', line 9 def (image, source, x: nil, y: nil, gravity: :center, opacity: 1.0, mode: :normal) source = ImageIO.read(source) unless source.is_a?(Tessel::Image) opacity = Float(opacity) mode = mode.to_sym raise ArgumentError, "opacity must be between 0 and 1" unless opacity.finite? && opacity.between?(0, 1) raise ArgumentError, "unknown blend mode: #{mode}" unless BLENDS.include?(mode) offset_x, offset_y = Gravity.offset(image.width, image.height, source.width, source.height, gravity) offset_x = Integer(x) unless x.nil? offset_y = Integer(y) unless y.nil? output = image.dup source.height.times do |source_y| source.width.times do |source_x| target_x = source_x + offset_x target_y = source_y + offset_y next unless target_x.between?(0, image.width - 1) && target_y.between?(0, image.height - 1) source_pixel = source[source_x, source_y] source_pixel[3] = (source_pixel[3] * opacity).round next if source_pixel[3].zero? output[target_x, target_y] = blend_pixel(output[target_x, target_y], source_pixel, mode) end end output end |
.pad(image, amount, color: [0, 0, 0, 0]) ⇒ Object
100 101 102 103 104 105 |
# File 'lib/retouch/operations.rb', line 100 def pad(image, amount, color: [0, 0, 0, 0]) amount = Integer(amount) raise ArgumentError, "padding must not be negative" if amount.negative? extend(image, image.width + (2 * amount), image.height + (2 * amount), color:, gravity: :center) end |
.pixelate(image, size) ⇒ Object
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/retouch/operations/filter.rb', line 34 def pixelate(image, size) size = Integer(size) raise ArgumentError, "pixel size must be positive" unless size.positive? return image.dup if image.width.zero? || image.height.zero? output = image.dup (0...image.height).step(size) do |y| height = [size, image.height - y].min (0...image.width).step(size) do |x| width = [size, image.width - x].min alpha_sum = red = green = blue = 0.0 (y...(y + height)).each do |py| (x...(x + width)).each do |px| color = image[px, py] alpha = color[3] / 255.0 red += color[0] * alpha green += color[1] * alpha blue += color[2] * alpha alpha_sum += color[3] end end count = width * height alpha = (alpha_sum / count).round color = alpha_sum.positive? ? [(red * 255 / alpha_sum).round, (green * 255 / alpha_sum).round, (blue * 255 / alpha_sum).round, alpha] : [0, 0, 0, 0] output.fill_rect(x, y, width, height, color, blend: :copy) end end output end |
.quantize(image, colors: 256, dither: :none, palette: nil) ⇒ Object
17 18 19 |
# File 'lib/retouch/operations/color.rb', line 17 def quantize(image, colors: 256, dither: :none, palette: nil) Kernels.quantize(image, colors:, dither:, palette:) end |
.rect(image, x, y, width, height, color = "#ffffff", fill: false) ⇒ Object
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/retouch/operations/draw.rb', line 40 def rect(image, x, y, width, height, color = "#ffffff", fill: false) x = Integer(x) y = Integer(y) width = Integer(width) height = Integer(height) raise ArgumentError, "rectangle dimensions must be positive" unless width.positive? && height.positive? output = image.dup if fill output.fill_rect(x, y, width, height, color, blend: :alpha) else output.fill_rect(x, y, width, 1, color, blend: :alpha) output.fill_rect(x, y + height - 1, width, 1, color, blend: :alpha) output.fill_rect(x, y, 1, height, color, blend: :alpha) output.fill_rect(x + width - 1, y, 1, height, color, blend: :alpha) end output end |
.resize(image, geometry, filter: :lanczos3, gravity: :center) ⇒ Object
29 30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/retouch/operations.rb', line 29 def resize(image, geometry, filter: :lanczos3, gravity: :center) geometry = Geometry.parse(geometry) unless geometry.is_a?(Geometry) width, height = geometry.target_size(image.width, image.height) output = resample(image, width, height, filter) if geometry.mode == :cover && geometry.width && geometry.height crop_width, crop_height = geometry.crop_size(image.width, image.height) x, y = Gravity.offset(output.width, output.height, crop_width, crop_height, gravity) x = geometry.offset_x unless geometry.offset_x.zero? y = geometry.offset_y unless geometry.offset_y.zero? output = output.crop(x.clamp(0, [output.width - crop_width, 0].max), y.clamp(0, [output.height - crop_height, 0].max), crop_width, crop_height) end output end |
.rotate(image, degrees, background: [0, 0, 0, 0]) ⇒ Object
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/retouch/operations.rb', line 69 def rotate(image, degrees, background: [0, 0, 0, 0]) angle = Float(degrees) % 360 raise ArgumentError, "degrees must be finite" unless angle.finite? return image.dup if angle.zero? return rotate_quarter(image, 1) if angle == 90 return rotate_quarter(image, 2) if angle == 180 return rotate_quarter(image, 3) if angle == 270 radians = angle * Math::PI / 180 cosine = Math.cos(radians) sine = Math.sin(radians) width = ((image.width * cosine.abs) + (image.height * sine.abs)).ceil height = ((image.width * sine.abs) + (image.height * cosine.abs)).ceil output = Tessel::Image.new(width, height, fill: background, metadata: image.) source_cx = (image.width - 1) / 2.0 source_cy = (image.height - 1) / 2.0 target_cx = (width - 1) / 2.0 target_cy = (height - 1) / 2.0 height.times do |y| width.times do |x| dx = x - target_cx dy = y - target_cy sx = (cosine * dx) + (sine * dy) + source_cx sy = (-sine * dx) + (cosine * dy) + source_cy output[x, y] = sample_bilinear(image, sx, sy, background) if sx.between?(0, image.width - 1) && sy.between?(0, image.height - 1) end end output end |
.saturate ⇒ Object
30 |
# File 'sig/retouch.rbs', line 30
def self.saturate: (untyped image, Numeric factor) -> untyped
|
.sharpen(image, sigma, amount: 1.0) ⇒ Object
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# File 'lib/retouch/operations/filter.rb', line 11 def sharpen(image, sigma, amount: 1.0) amount = Float(amount) raise ArgumentError, "sharpen amount must be between 0 and 10" unless amount.finite? && amount.between?(0, 10) = blur(image, sigma) source = image.bytes smooth = .bytes output = source.dup (0...(image.width * image.height)).each do |index| offset = index * 4 original_alpha = source.getbyte(offset + 3) / 255.0 smooth_alpha = smooth.getbyte(offset + 3) / 255.0 3.times do |channel| original = source.getbyte(offset + channel) * original_alpha = smooth.getbyte(offset + channel) * smooth_alpha sharpened = original + (amount * (original - )) color = original_alpha.positive? ? (sharpened / original_alpha).round.clamp(0, 255) : 0 output.setbyte(offset + channel, color) end end Tessel::Image.from_rgba(image.width, image.height, output, metadata: image.) end |
.spritesheet(images, max_width: 2048, gap: 0, background: [0, 0, 0, 0]) ⇒ [untyped, Array[Hash[Symbol, Integer]]]
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/retouch/operations/multiple.rb', line 69 def spritesheet(images, max_width: 2048, gap: 0, background: [0, 0, 0, 0]) images = validate_images(images) max_width = Integer(max_width) gap = Integer(gap) raise ArgumentError, "max_width must be positive" unless max_width.positive? raise ArgumentError, "gap must not be negative" if gap.negative? raise ArgumentError, "an image is wider than max_width" if images.any? { |image| image.width > max_width } bound_height = images.sum(&:height) + (gap * images.length) free = [{ x: 0, y: 0, width: max_width, height: bound_height }] placements = Array.new(images.length) images.each_with_index.sort_by { |image, index| [-image.width * image.height, -[image.width, image.height].max, index] }.each do |image, index| packed_width = image.width + gap packed_height = image.height + gap position = free.select { |rect| packed_width <= rect[:width] && packed_height <= rect[:height] } .map do |rect| leftover_width = rect[:width] - packed_width leftover_height = rect[:height] - packed_height score = [ [leftover_width, leftover_height].min, rect[:y], rect[:x], [leftover_width, leftover_height].max ] [score, rect[:x], rect[:y]] end.min_by(&:first) raise ArgumentError, "could not pack images within max_width" unless position _score, x, y = position packed = { x:, y:, width: packed_width, height: packed_height } placements[index] = { index:, x:, y:, width: image.width, height: image.height } free = split_free_rectangles(free, packed) end used_width = placements.map { |item| item[:x] + item[:width] }.max || 0 height = placements.map { |item| item[:y] + item[:height] }.max || 0 output = Tessel::Image.new(used_width, height, fill: background) images.zip(placements).each { |image, item| output.blit(image, item[:x], item[:y]) } [output, placements] end |
.text(image, string, x: 0, y: 0, at: nil, size: 16, font: nil, color: "#ffffff", background: nil, padding: 4) ⇒ Object
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/retouch/operations/draw.rb', line 78 def text(image, string, x: 0, y: 0, at: nil, size: 16, font: nil, color: "#ffffff", background: nil, padding: 4) begin require "glyphic" rescue LoadError => e raise Error, "text requires the glyphic gem (gem install glyphic)", cause: e end size = Integer(size) raise ArgumentError, "font size must be positive" unless size.positive? font_object = font ? Glyphic.load(font, size: Integer(size)) : Glyphic.default rendered = font_object.render(String(string), color: Tessel::Color.pack(color).bytes) if !font && size != font_object.line_height scale = size.to_f / font_object.line_height rendered = rendered.scale_nearest([(rendered.width * scale).round, 1].max, [(rendered.height * scale).round, 1].max) end text_width = rendered.width text_height = rendered.height padding = Integer(padding) raise ArgumentError, "text padding must not be negative" if padding.negative? if at x, y = Gravity.offset(image.width, image.height, text_width + (padding * 2), text_height + (padding * 2), at) x += padding y += padding else x = Integer(x) y = Integer(y) end output = image.dup output.fill_rect(x - padding, y - padding, text_width + (padding * 2), text_height + (padding * 2), background, blend: :alpha) if background output.blit(rendered, x, y) output end |
.thumbnail(image, geometry, **options) ⇒ Object
43 44 45 46 |
# File 'lib/retouch/operations.rb', line 43 def thumbnail(image, geometry, **) result = resize(image, geometry, **) Tessel::Image.from_rgba(result.width, result.height, result.bytes) end |
.tint(image, color, amount: 1.0) ⇒ Object
13 14 15 |
# File 'lib/retouch/operations/color.rb', line 13 def tint(image, color, amount: 1.0) Kernels::LUT.apply(image, :tint, color, amount: Float(amount)) end |
.trim(image, color: nil, fuzz: 0) ⇒ Object
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/retouch/operations.rb', line 124 def trim(image, color: nil, fuzz: 0) fuzz = Integer(fuzz) raise ArgumentError, "fuzz must be between 0 and 255" unless fuzz.between?(0, 255) target = Tessel::Color.pack(color || image[0, 0] || [0, 0, 0, 0]).bytes bounds = [image.width, image.height, -1, -1] image.height.times do |y| image.width.times do |x| pixel = image[x, y] next if pixel.zip(target).all? { |a, b| (a - b).abs <= fuzz } bounds[0] = [bounds[0], x].min bounds[1] = [bounds[1], y].min bounds[2] = [bounds[2], x].max bounds[3] = [bounds[3], y].max end end return image.dup if bounds[2].negative? (image.crop(bounds[0], bounds[1], bounds[2] - bounds[0] + 1, bounds[3] - bounds[1] + 1), image) end |
.watermark(image, source, **options) ⇒ Object
36 37 38 |
# File 'lib/retouch/operations/draw.rb', line 36 def watermark(image, source, **) (image, source, **) end |