diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a2073f..d15eeb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## Rszr 1.3.0 (unreleased) + +* Alpha channel control +* Background initialization +* Image blending / watermarking +* Rectangle and image fills +* Color gradients +* Hex color codes always prefixed by "#" + ## Rszr 1.2.1 (Mar 22, 2022) * Fix saving without extension (@mantas) diff --git a/README.md b/README.md index 4bb9988..9836543 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,10 @@ image.width => 400 image.height => 300 image.dimensions => [400, 300] image.format => "jpeg" +image.alpha? => false image[0, 0] => -image[0, 0].to_hex => "26738dff" -image[0, 0].to_hex(rgb: true) => "26738d" +image[0, 0].to_hex => "#26738dff" +image[0, 0].to_hex(alpha: false) => "#26738d" ``` ### Transformations @@ -124,6 +125,97 @@ image.flop image.dup ``` +### Image generation + +```ruby +# generate new image with transparent background +image = Rszr::Image.new(500, 500, alpha: true, background: Rszr::Color::Transparent) + +# fill image with 50% opacity +image.fill!(Rszr::Color::RGBA.new(0, 206, 209, 50)) + +# define a color gradient +gradient = Rszr::Color::Gradient.new do |g| + g.point 0, 255, 250, 205, 50 + g.point 0.5, 135, 206, 250 + g.point 1, Rszr::Color::White +end + +# draw a rectangle and fill it using the gradient with 45° +image.rectangle!(gradient.to_fill(45), 100, 100, 300, 300) +``` + +### Colors + +```ruby +# pre-defined colors +Rszr::Color::White +Rszr::Color::Black +Rszr::Color::Transparent + +# RGB +color = Rszr::Color.rgba(255, 250, 50) +color.red => 255 +color.green => 250 +color.blue => 50 +color.alpha => 255 +color.cyan => 0 +color.magenta => 5 +color.yellow => 205 + +# RGBA +Rszr::Color.rgba(255, 250, 50, 255) + +# CMY +Rszr::Color.cmya(0, 5, 205) + +# CMYA +Rszr::Color.cmya(0, 5, 205, 255) +``` + +### Color gradients + +```ruby +# three-color linear gradient with changing opacity +gradient = Rszr::Color::Gradient.new do |g| + g.point 0, 255, 250, 205, 50 + g.point 0.5, 135, 206, 250 + g.point 1, Rszr::Color::White +end + +# alternative syntax +gradient = Rszr::Color::Gradient.new(0 => "#fffacd32", 0.5 => "#87cefa", 1 => "#fff") + +# generate fill with 45° angle +fill = gradient.to_fill(45) + +# use as image background +image = Rszr::Image.new(500, 500, background: fill) +``` + +### Watermarking and image blending + +```ruby +# load logo +logo = Rszr::Image.load('logo.png') + +# load image +image = Rszr::Image.load('image.jpg') + +# enable alpha channel +image.alpha = true + +# blend it onto the image at position (10, 10) +image.blend!(logo, 10, 10) + +# blending modes: +# - copy (default) +# - add +# - subtract +# - reshade +image.blend(logo, 10, 10, mode: :subtract) +``` + ### Filters Filters also support bang! and non-bang methods. @@ -173,8 +265,7 @@ In order to save interlaced PNGs and progressive JPEGs, set the `interlace` opti image.save('interlaced.png', interlace: true) ``` -As of v1.8.0, `imlib2` doesn't support saving progressive JPEG images yet, -but a [patch](https://git.enlightenment.org/legacy/imlib2.git/commit/?id=37e8c9578897259211284d3590cc38b7f6a718dc) has been submitted. +Saving progressive JPEG images requires `imlib2` >= 1.8.1. For EL8, there are pre-built RPMs provided by the [onrooby repo](http://downloads.onrooby.com/repo/el/8/x86_64/). diff --git a/ext/rszr/errors.c b/ext/rszr/errors.c index 6ebec83..f2cbb27 100644 --- a/ext/rszr/errors.c +++ b/ext/rszr/errors.c @@ -5,6 +5,7 @@ #include "errors.h" VALUE eRszrError = Qnil; +VALUE eRszrInternalError = Qnil; VALUE eRszrFileNotFound = Qnil; VALUE eRszrTransformationError = Qnil; VALUE eRszrErrorWithMessage = Qnil; @@ -33,11 +34,12 @@ const int RSZR_MAX_ERROR_INDEX = 13; void Init_rszr_errors() { eRszrError = rb_define_class_under(mRszr, "Error", rb_eStandardError); + eRszrInternalError = rb_define_class_under(mRszr, "InternalError", eRszrError); eRszrFileNotFound = rb_define_class_under(mRszr, "FileNotFound", eRszrError); eRszrTransformationError = rb_define_class_under(mRszr, "TransformationError", eRszrError); eRszrErrorWithMessage = rb_define_class_under(mRszr, "ErrorWithMessage", eRszrError); eRszrLoadError = rb_define_class_under(mRszr, "LoadError", eRszrErrorWithMessage); - eRszrSaveError = rb_define_class_under(mRszr, "SaveError", eRszrErrorWithMessage); + eRszrSaveError = rb_define_class_under(mRszr, "SaveError", eRszrErrorWithMessage); } static void rszr_raise_error_with_message(VALUE rb_error_class, Imlib_Load_Error error) diff --git a/ext/rszr/image.c b/ext/rszr/image.c index 622906e..3655802 100644 --- a/ext/rszr/image.c +++ b/ext/rszr/image.c @@ -6,6 +6,10 @@ #include "errors.h" VALUE cImage = Qnil; +VALUE cColorBase = Qnil; +VALUE cColorGradient = Qnil; +VALUE cColorPoint = Qnil; +VALUE cFill = Qnil; static void rszr_free_image(Imlib_Image image) @@ -34,7 +38,7 @@ static VALUE rszr_image_s_allocate(VALUE klass) } -static VALUE rszr_image_initialize(VALUE self, VALUE rb_width, VALUE rb_height) +static VALUE rszr_image__initialize(VALUE self, VALUE rb_width, VALUE rb_height) { rszr_image_handle * handle; @@ -98,7 +102,6 @@ static VALUE rszr_image__format_get(VALUE self) } } - static VALUE rszr_image__format_set(VALUE self, VALUE rb_format) { rszr_image_handle * handle; @@ -113,6 +116,51 @@ static VALUE rszr_image__format_set(VALUE self, VALUE rb_format) } +static void rszr_image_color_set(VALUE rb_color) +{ + int r, g, b, a; + + if(!rb_obj_is_kind_of(rb_color, cColorBase) || RBASIC_CLASS(rb_color) == cColorBase) { + rb_raise(rb_eArgError, "color must descend from Rszr::Color::Base"); + } + + r = FIX2INT(rb_funcall(rb_color, rb_intern("red"), 0)); + g = FIX2INT(rb_funcall(rb_color, rb_intern("green"), 0)); + b = FIX2INT(rb_funcall(rb_color, rb_intern("blue"), 0)); + a = FIX2INT(rb_funcall(rb_color, rb_intern("alpha"), 0)); + + // TODO: use color model specific setter function + imlib_context_set_color(r, g, b, a); +} + + +static VALUE rszr_image_alpha_get(VALUE self) +{ + rszr_image_handle * handle; + + Data_Get_Struct(self, rszr_image_handle, handle); + + imlib_context_set_image(handle->image); + if (imlib_image_has_alpha()) { + return Qtrue; + } + + return Qfalse; +} + +static VALUE rszr_image_alpha_set(VALUE self, VALUE rb_alpha) +{ + rszr_image_handle * handle; + + Data_Get_Struct(self, rszr_image_handle, handle); + + imlib_context_set_image(handle->image); + imlib_image_set_has_alpha(RTEST(rb_alpha) ? 1 : 0); + + return Qnil; +} + + static VALUE rszr_image_width(VALUE self) { rszr_image_handle * handle; @@ -357,6 +405,7 @@ static Imlib_Image rszr_create_cropped_scaled_image(const Imlib_Image image, VAL imlib_context_set_image(image); imlib_context_set_anti_alias(1); + imlib_context_set_dither(1); resized_image = imlib_create_cropped_scaled_image(src_x, src_y, src_w, src_h, dst_w, dst_h); if (!resized_image) { @@ -400,6 +449,11 @@ static Imlib_Image rszr_create_cropped_image(const Imlib_Image image, VALUE rb_x { Imlib_Image cropped_image; + Check_Type(rb_x, T_FIXNUM); + Check_Type(rb_y, T_FIXNUM); + Check_Type(rb_w, T_FIXNUM); + Check_Type(rb_h, T_FIXNUM); + int x = NUM2INT(rb_x); int y = NUM2INT(rb_y); int w = NUM2INT(rb_w); @@ -445,6 +499,129 @@ static VALUE rszr_image__crop(VALUE self, VALUE bang, VALUE rb_x, VALUE rb_y, VA } +static VALUE rszr_image__blend(VALUE self, VALUE other, VALUE rb_merge_alpha, VALUE rb_mode, + VALUE rb_src_x, VALUE rb_src_y, VALUE rb_src_w, VALUE rb_src_h, + VALUE rb_dst_x, VALUE rb_dst_y, VALUE rb_dst_w, VALUE rb_dst_h) +{ + rszr_image_handle * handle; + rszr_image_handle * other_handle; + Imlib_Operation operation; + + Check_Type(rb_mode, T_FIXNUM); + Check_Type(rb_src_x, T_FIXNUM); + Check_Type(rb_src_y, T_FIXNUM); + Check_Type(rb_src_w, T_FIXNUM); + Check_Type(rb_src_h, T_FIXNUM); + Check_Type(rb_dst_x, T_FIXNUM); + Check_Type(rb_dst_y, T_FIXNUM); + Check_Type(rb_dst_w, T_FIXNUM); + Check_Type(rb_dst_h, T_FIXNUM); + + operation = (Imlib_Operation) NUM2INT(rb_mode); + int src_x = NUM2INT(rb_src_x); + int src_y = NUM2INT(rb_src_y); + int src_w = NUM2INT(rb_src_w); + int src_h = NUM2INT(rb_src_h); + int dst_x = NUM2INT(rb_dst_x); + int dst_y = NUM2INT(rb_dst_y); + int dst_w = NUM2INT(rb_dst_w); + int dst_h = NUM2INT(rb_dst_h); + + char merge_alpha = RTEST(rb_merge_alpha) ? 1 : 0; + + Data_Get_Struct(self, rszr_image_handle, handle); + Data_Get_Struct(other, rszr_image_handle, other_handle); + + imlib_context_set_image(handle->image); + imlib_context_set_operation(operation); + imlib_blend_image_onto_image(other_handle->image, merge_alpha, src_x, src_y, src_w, src_h, dst_x, dst_y, dst_w, dst_h); + + return self; +} + + +static Imlib_Color_Range rszr_image_init_color_range(VALUE rb_gradient) +{ + Imlib_Color_Range range; + VALUE rb_points; + VALUE rb_point; + VALUE rb_color; + int size, i; + double position; + int red, green, blue, alpha; + + if(!rb_obj_is_kind_of(rb_gradient, cColorGradient)) { + rb_raise(rb_eArgError, "color must be a Rszr::Color::Gradient"); + } + + rb_points = rb_funcall(rb_gradient, rb_intern("points"), 0); + Check_Type(rb_points, T_ARRAY); + + imlib_context_get_color(&red, &green, &blue, &alpha); + + range = imlib_create_color_range(); + imlib_context_set_color_range(range); + + size = RARRAY_LEN(rb_points); + for (i = 0; i < size; i++) { + rb_point = rb_ary_entry(rb_points, i); + if(!rb_obj_is_kind_of(rb_point, cColorPoint)) + rb_raise(rb_eArgError, "point must be a Rszr::Color::Point"); + + rb_color = rb_funcall(rb_point, rb_intern("color"), 0); + if(!rb_obj_is_kind_of(rb_color, cColorBase) || RBASIC_CLASS(rb_color) == cColorBase) + rb_raise(rb_eArgError, "color must descend from Rszr::Color::Base"); + + position = NUM2DBL(rb_funcall(rb_point, rb_intern("position"), 0)); + + rszr_image_color_set(rb_color); + imlib_add_color_to_color_range(position * 255); + } + + imlib_context_set_color(red, green, blue, alpha); + + return range; +} + + +static VALUE rszr_image__rectangle_bang(VALUE self, VALUE rb_fill, VALUE rb_x, VALUE rb_y, VALUE rb_w, VALUE rb_h) +{ + rszr_image_handle * handle; + VALUE rb_gradient; + VALUE rb_color; + Imlib_Color_Range range; + double angle; + + Check_Type(rb_x, T_FIXNUM); + Check_Type(rb_y, T_FIXNUM); + Check_Type(rb_w, T_FIXNUM); + Check_Type(rb_h, T_FIXNUM); + + int x = NUM2INT(rb_x); + int y = NUM2INT(rb_y); + int w = NUM2INT(rb_w); + int h = NUM2INT(rb_h); + + rb_gradient = rb_funcall(rb_fill, rb_intern("gradient"), 0); + rb_color = rb_funcall(rb_fill, rb_intern("color"), 0); + + Data_Get_Struct(self, rszr_image_handle, handle); + imlib_context_set_image(handle->image); + + if (!NIL_P(rb_gradient)) { + angle = NUM2DBL(rb_funcall(rb_fill, rb_intern("angle"), 0)); + range = rszr_image_init_color_range(rb_gradient); + imlib_image_fill_color_range_rectangle(x, y, w, h, angle); + imlib_free_color_range(); + } else if (!NIL_P(rb_color)) { + rszr_image_color_set(rb_color); + imlib_image_fill_rectangle(x, y, w, h); + } + + return self; +} + + static VALUE rszr_image__save(VALUE self, VALUE rb_path, VALUE rb_format, VALUE rb_quality, VALUE rb_interlace) { rszr_image_handle * handle; @@ -461,6 +638,7 @@ static VALUE rszr_image__save(VALUE self, VALUE rb_path, VALUE rb_format, VALUE imlib_context_set_image(handle->image); imlib_image_set_format(format); + if (quality) imlib_image_attach_data_value("quality", NULL, quality, NULL); @@ -481,13 +659,18 @@ static VALUE rszr_image__save(VALUE self, VALUE rb_path, VALUE rb_format, VALUE void Init_rszr_image() { cImage = rb_define_class_under(mRszr, "Image", rb_cObject); + + cColorBase = rb_path2class("Rszr::Color::Base"); + cColorGradient = rb_path2class("Rszr::Color::Gradient"); + cColorPoint = rb_path2class("Rszr::Color::Point"); + cFill = rb_path2class("Rszr::Fill"); + rb_define_alloc_func(cImage, rszr_image_s_allocate); // Class methods rb_define_private_method(rb_singleton_class(cImage), "_load", rszr_image_s__load, 1); // Instance methods - rb_define_method(cImage, "initialize", rszr_image_initialize, 2); rb_define_method(cImage, "width", rszr_image_width, 0); rb_define_method(cImage, "height", rszr_image_height, 0); rb_define_method(cImage, "dup", rszr_image_dup, 0); @@ -495,19 +678,22 @@ void Init_rszr_image() rb_define_method(cImage, "flop!", rszr_image_flop_bang, 0); rb_define_method(cImage, "flip!", rszr_image_flip_bang, 0); - // rb_define_method(cImage, "quality", rszr_image_get_quality, 0); - // rb_define_method(cImage, "quality=", rszr_image_set_quality, 1); + rb_define_method(cImage, "alpha", rszr_image_alpha_get, 0); + rb_define_method(cImage, "alpha=", rszr_image_alpha_set, 1); rb_define_protected_method(cImage, "_format", rszr_image__format_get, 0); rb_define_protected_method(cImage, "_format=", rszr_image__format_set, 1); - - rb_define_private_method(cImage, "_resize", rszr_image__resize, 7); - rb_define_private_method(cImage, "_crop", rszr_image__crop, 5); - rb_define_private_method(cImage, "_turn!", rszr_image__turn_bang, 1); - rb_define_private_method(cImage, "_rotate", rszr_image__rotate, 2); - rb_define_private_method(cImage, "_sharpen!", rszr_image__sharpen_bang, 1); - rb_define_private_method(cImage, "_pixel", rszr_image__pixel_get, 2); - + + rb_define_private_method(cImage, "_initialize", rszr_image__initialize, 2); + rb_define_private_method(cImage, "_resize", rszr_image__resize, 7); + rb_define_private_method(cImage, "_crop", rszr_image__crop, 5); + rb_define_private_method(cImage, "_turn!", rszr_image__turn_bang, 1); + rb_define_private_method(cImage, "_rotate", rszr_image__rotate, 2); + rb_define_private_method(cImage, "_sharpen!", rszr_image__sharpen_bang, 1); + rb_define_private_method(cImage, "_pixel", rszr_image__pixel_get, 2); + rb_define_private_method(cImage, "_blend", rszr_image__blend, 11); + rb_define_private_method(cImage, "_rectangle!", rszr_image__rectangle_bang, 5); + rb_define_private_method(cImage, "_save", rszr_image__save, 4); } diff --git a/lib/rszr.rb b/lib/rszr.rb index 8a77144..1b43730 100644 --- a/lib/rszr.rb +++ b/lib/rszr.rb @@ -3,13 +3,14 @@ require 'tempfile' require 'stringio' -require 'rszr/rszr' require 'rszr/version' require 'rszr/stream' require 'rszr/identification' require 'rszr/orientation' require 'rszr/buffered' require 'rszr/color' +require 'rszr/fill' +require 'rszr/rszr' require 'rszr/image' module Rszr diff --git a/lib/rszr/color.rb b/lib/rszr/color.rb index 9e911fa..32afb8d 100644 --- a/lib/rszr/color.rb +++ b/lib/rszr/color.rb @@ -1,25 +1,15 @@ +require_relative 'color/base' +require_relative 'color/rgba' +require_relative 'color/cmya' +require_relative 'color/point' +require_relative 'color/gradient' + module Rszr module Color - class RGBA - attr_reader :red, :green, :blue, :alpha - - def initialize(red, green, blue, alpha = 255) - if red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255 || alpha < 0 || alpha > 255 - raise ArgumentError, 'color out of range' - end - @red, @green, @blue, @alpha = red, green, blue, alpha - end - - def to_i(rgb: false) - i = red.to_i << 24 | green.to_i << 16 | blue.to_i << 8 | alpha.to_i - rgb ? i >> 8 : i - end - - def to_hex(rgb: false) - "%0#{rgb ? 6 : 8}x" % to_i(rgb: rgb) - end - end + Transparent = RGBA.new(0, 0, 0, 0) + White = RGBA.new(255,255,255) + Black = RGBA.new(0, 0, 0) end end diff --git a/lib/rszr/color/base.rb b/lib/rszr/color/base.rb new file mode 100644 index 0000000..bc0d070 --- /dev/null +++ b/lib/rszr/color/base.rb @@ -0,0 +1,25 @@ +module Rszr + module Color + + class Base + attr_reader :alpha + + def rgba + [red, green, blue, alpha] + end + + def cmya + [cyan, magenta, yellow, alpha] + end + + def ==(other) + other.is_a?(Base) && rgba == other.rgba + end + + def to_fill(*) + Fill.new(color: self) + end + end + + end +end diff --git a/lib/rszr/color/cmya.rb b/lib/rszr/color/cmya.rb new file mode 100644 index 0000000..dc23dc3 --- /dev/null +++ b/lib/rszr/color/cmya.rb @@ -0,0 +1,29 @@ +module Rszr + module Color + + class CMYA < Base + attr_reader :cyan, :magenta, :yellow + + def initialize(cyan, magenta, yellow, alpha = 255) + if cyan < 0 || cyan > 255 || magenta < 0 || magenta > 255 || yellow < 0 || yellow > 255 || alpha < 0 || alpha > 255 + raise ArgumentError, 'color out of range' + end + @cyan, @magenta, @yellow = cyan, magenta, yellow + end + + def red + 255 - cyan + end + + def green + 255 - magenta + end + + def blue + 255 - yellow + end + + end + + end +end diff --git a/lib/rszr/color/gradient.rb b/lib/rszr/color/gradient.rb new file mode 100644 index 0000000..d022b94 --- /dev/null +++ b/lib/rszr/color/gradient.rb @@ -0,0 +1,42 @@ +module Rszr + module Color + + class Gradient + attr_reader :points + + def initialize(*args) + @points = [] + points = args.last.is_a?(Hash) ? args.pop.dup : {} + args.each { |point| self << point } + points.each { |pos, color| point(pos, color) } + yield self if block_given? + end + + def initialize_dup(other) # :nodoc: + @points = other.points.map(&:dup) + end + + def <<(position, red = nil, green = nil, blue= nil, alpha = 255) + point = if red.is_a?(Point) + red + elsif red.is_a?(Color::Base) + Point.new(position, red) + elsif red.is_a?(String) && red.start_with?('#') + Point.new(position, Color.hex(red)) + else + Point.new(position, RGBA.new(red, green, blue, alpha)) + end + points << point + points.sort! + end + + alias_method :point, :<< + + def to_fill(angle = 0) + Fill.new(gradient: self, angle: angle) + end + + end + + end +end diff --git a/lib/rszr/color/point.rb b/lib/rszr/color/point.rb new file mode 100644 index 0000000..9ffc9a0 --- /dev/null +++ b/lib/rszr/color/point.rb @@ -0,0 +1,29 @@ +module Rszr + module Color + + class Point + attr_reader :position, :color + + class << self + def prgba(position, red, green, blue, alpha = 255) + new(position, RGBA.new(red, green, blue, alpha)) + end + end + + def initialize(position, color) + raise ArgumentError, 'position must be within 0..1' unless (0..1).cover?(position) + raise ArgumentError, 'color must be a Rszr::Color::Base' unless color.is_a?(Rszr::Color::Base) + @position, @color = position, color + end + + def <=>(other) + position <=> other.position + end + + def prgba + [position, *color.rgba] + end + end + + end +end diff --git a/lib/rszr/color/rgba.rb b/lib/rszr/color/rgba.rb new file mode 100644 index 0000000..9b41245 --- /dev/null +++ b/lib/rszr/color/rgba.rb @@ -0,0 +1,56 @@ +module Rszr + module Color + + class << self + def rgba(red, green, blue, alpha = 255) + RGBA.new(red, green, blue, alpha) + end + + def hex(str) + str = str[1..-1] if str.start_with?('#') + case str.size + when 3, 4 then hex(str.chars.map { |c| c * 2 }.join) + when 6 then hex("#{str}ff") + when 8 + rgba(*str.scan(/../).map(&:hex)) + else + raise ArgumentError, 'invalid color code' + end + end + end + + class RGBA < Base + attr_reader :red, :green, :blue + + def initialize(red, green, blue, alpha = 255) + if red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255 || alpha < 0 || alpha > 255 + raise ArgumentError, 'color out of range' + end + @red, @green, @blue, @alpha = red, green, blue, alpha + end + + def cyan + 255 - red + end + + def magenta + 255 - green + end + + def yellow + 255 - blue + end + + def to_i(alpha: true) + i = red.to_i << 24 | green.to_i << 16 | blue.to_i << 8 | self.alpha.to_i + alpha ? i : i >> 8 + end + + def to_hex(alpha: true) + "#%0#{alpha ? 8 : 6}x" % to_i(alpha: alpha) + end + + end + + end +end diff --git a/lib/rszr/fill.rb b/lib/rszr/fill.rb new file mode 100644 index 0000000..aedaa7e --- /dev/null +++ b/lib/rszr/fill.rb @@ -0,0 +1,21 @@ +module Rszr + class Fill + attr_reader :color, :gradient, :angle + + def initialize(color: nil, gradient: nil, angle: 0) + if gradient + @gradient = gradient + @angle = angle || 0 + elsif color + @color = color + else + raise ArgumentError, 'incomplete fill definition' + end + end + + def to_fill(*) + self + end + + end +end diff --git a/lib/rszr/image.rb b/lib/rszr/image.rb index 59ff2e6..287c56d 100644 --- a/lib/rszr/image.rb +++ b/lib/rszr/image.rb @@ -1,6 +1,7 @@ module Rszr class Image GRAVITIES = [true, :center, :n, :nw, :w, :sw, :s, :se, :e, :ne].freeze + BLENDING_MODES = %i[copy add subtract reshade].freeze extend Identification include Buffered @@ -40,6 +41,8 @@ def format=(fmt) self._format = fmt end + alias_method :alpha?, :alpha + def [](x, y) if x >= 0 && x <= width - 1 && y >= 0 && y <= height - 1 Color::RGBA.new(*_pixel(x, y)) @@ -49,7 +52,7 @@ def [](x, y) def inspect fmt = format fmt = " #{fmt.upcase}" if fmt - "#<#{self.class.name}:0x#{object_id.to_s(16)} #{width}x#{height}#{fmt}>" + "#<#{self.class.name}:0x#{object_id.to_s(16)} #{width}x#{height}x#{alpha? ? 32 : 24}#{fmt}>" end module Transformations @@ -144,10 +147,46 @@ def gamma!(value, r: nil, g: nil, b: nil, a: nil) def gamma(*args, **opts) dup.gamma!(*args, **opts) end + + def blend!(image, x, y, mode: :copy) + raise ArgumentError, "mode must be one of #{BLENDING_MODES.map(&:to_s).join(', ')}" unless BLENDING_MODES.include?(mode) + _blend(image, true, BLENDING_MODES.index(mode), 0, 0, image.width, image.height, x, y, image.width, image.height) + end + + def blend(*args, **opts) + dup.blend!(*args, **opts) + end + + def rectangle!(coloring, x, y, w, h) + raise ArgumentError, "coloring must respond to to_fill" unless coloring.respond_to?(:to_fill) + _rectangle!(coloring.to_fill, x, y, w, h) + end + + def rectangle(*args, **opts) + dup.rectangle!(*args, **opts) + end + + def fill!(coloring) + raise ArgumentError, "coloring must respond to to_fill" unless coloring.respond_to?(:to_fill) + rectangle!(coloring, 0, 0, width, height) + end + + def fill(*args, **opts) + dup.fill(*args, **opts) + end end include Transformations + def initialize(width, height, alpha: false, background: nil) + raise ArgumentError, 'illegal image dimensions' if width < 1 || width > 32766 || height < 1 || height > 32766 + raise ArgumentError, 'background must respond to to_fill' if background && !(background.respond_to?(:to_fill)) + _initialize(width, height).tap do |image| + image.alpha = alpha + image.fill!(background) if background + end + end + def save(path, format: nil, quality: nil, interlace: false) format ||= format_from_filename(path) || self.format || 'jpg' raise ArgumentError, "invalid quality #{quality.inspect}" if quality && !(0..100).cover?(quality) diff --git a/lib/rszr/version.rb b/lib/rszr/version.rb index 63d0925..3e1b1db 100644 --- a/lib/rszr/version.rb +++ b/lib/rszr/version.rb @@ -1,3 +1,3 @@ module Rszr - VERSION = '1.2.1' + VERSION = '1.3.0' end diff --git a/spec/rszr_spec.rb b/spec/rszr_spec.rb index bca8801..a29082b 100644 --- a/spec/rszr_spec.rb +++ b/spec/rszr_spec.rb @@ -53,11 +53,11 @@ end it 'provide pixel RGBA value' do - expect(@image[0, 0].to_hex).to eq('4c5c6cff') + expect(@image[0, 0].to_hex).to eq('#4c5c6cff') end it 'provide pixel RGB value' do - expect(@image[0, 0].to_hex(rgb: true)).to eq('4c5c6c') + expect(@image[0, 0].to_hex(alpha: false)).to eq('#4c5c6c') end it 'return nil if pixel out of bounds' do