From b7a6ddc8fadb22132d963d00732aca18e1a16edf Mon Sep 17 00:00:00 2001 From: Matthias Grosser Date: Tue, 15 Mar 2022 21:53:03 +0100 Subject: [PATCH 1/4] Image blending --- CHANGELOG.md | 5 +- ext/rszr/image.c | 158 ++++++++++++++++++++++++++++++++++++++++---- lib/rszr/color.rb | 56 ++++++++++++++-- lib/rszr/image.rb | 19 +++++- lib/rszr/version.rb | 2 +- 5 files changed, 219 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e338aa..7bcf6ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ -## Rszr 1.2.1 (unreleased) +## Rszr 1.3.0 (unreleased) * Fix saving without extension (@mantas) +* Alpha channel control +* Background initialization +* Image blending ## Rszr 1.2.0 (Mar 11, 2022) diff --git a/ext/rszr/image.c b/ext/rszr/image.c index 622906e..1677ab8 100644 --- a/ext/rszr/image.c +++ b/ext/rszr/image.c @@ -34,7 +34,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 +98,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 +112,54 @@ static VALUE rszr_image__format_set(VALUE self, VALUE rb_format) } +static void rszr_image_color_set(VALUE rb_Color) +{ + VALUE rb_cColorBase; + int r, g, b, a; + + rb_cColorBase = rb_path2class("Rszr::Color::Base"); + + if(!rb_obj_is_kind_of(rb_Color, rb_cColorBase) || RBASIC_CLASS(rb_Color) == rb_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 +404,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 +448,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 +498,85 @@ 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_operation, 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 VALUE rszr_image_rectangle_bang(VALUE self, VALUE rb_color, VALUE rb_x, VALUE rb_y, VALUE rb_w, VALUE rb_h) +{ + rszr_image_handle * handle; + + 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); + + Data_Get_Struct(self, rszr_image_handle, handle); + + imlib_context_set_image(handle->image); + rszr_image_color_set(rb_color); + imlib_image_fill_rectangle(x, y, w, h); + + return self; +} + + +static VALUE rszr_image_fill_bang(VALUE self, VALUE rb_color) +{ + rszr_image_handle * handle; + + Data_Get_Struct(self, rszr_image_handle, handle); + + imlib_context_set_image(handle->image); + rszr_image_color_set(rb_color); + imlib_image_fill_rectangle(0, 0, imlib_image_get_width(), imlib_image_get_height()); + + 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; @@ -487,27 +619,29 @@ void Init_rszr_image() 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); rb_define_method(cImage, "filter!", rszr_image_filter_bang, 1); rb_define_method(cImage, "flop!", rszr_image_flop_bang, 0); rb_define_method(cImage, "flip!", rszr_image_flip_bang, 0); + rb_define_method(cImage, "rectangle!", rszr_image_rectangle_bang, 5); + rb_define_method(cImage, "fill!", rszr_image_fill_bang, 1); - // 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, "_save", rszr_image__save, 4); } diff --git a/lib/rszr/color.rb b/lib/rszr/color.rb index 9e911fa..3f8f866 100644 --- a/lib/rszr/color.rb +++ b/lib/rszr/color.rb @@ -1,8 +1,12 @@ module Rszr module Color - class RGBA - attr_reader :red, :green, :blue, :alpha + class Base + attr_reader :alpha + 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 @@ -10,16 +14,56 @@ def initialize(red, green, blue, alpha = 255) end @red, @green, @blue, @alpha = red, green, blue, alpha end - - def to_i(rgb: false) + + 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 | alpha.to_i - rgb ? i >> 8 : i + alpha ? i : i >> 8 end - + def to_hex(rgb: false) "%0#{rgb ? 6 : 8}x" % to_i(rgb: rgb) end end + 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 + + 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/image.rb b/lib/rszr/image.rb index 59ff2e6..3f1c535 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,24 @@ def gamma!(value, r: nil, g: nil, b: nil, a: nil) def gamma(*args, **opts) dup.gamma!(*args, **opts) end + + def blend(image, 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, 0, 0, image.width, image.height) + 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 descend from Rszr::Color::Base' if background && !(background.class < Color::Base) + _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 From 87f13b2be2db9db8473a734a79f91a7dc0eae06a Mon Sep 17 00:00:00 2001 From: Matthias Grosser Date: Tue, 15 Mar 2022 22:22:38 +0100 Subject: [PATCH 2/4] Fix variable name --- ext/rszr/image.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/rszr/image.c b/ext/rszr/image.c index 1677ab8..1d60a6f 100644 --- a/ext/rszr/image.c +++ b/ext/rszr/image.c @@ -506,7 +506,7 @@ static VALUE rszr_image__blend(VALUE self, VALUE other, VALUE rb_merge_alpha, VA rszr_image_handle * other_handle; Imlib_Operation operation; - Check_Type(rb_operation, T_FIXNUM); + 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); @@ -593,6 +593,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); From df87e2af6790bc3bdcab7e3829c7b8dd7152c2b7 Mon Sep 17 00:00:00 2001 From: Matthias Grosser Date: Fri, 25 Mar 2022 12:25:21 +0100 Subject: [PATCH 3/4] Gradients --- CHANGELOG.md | 2 + README.md | 33 ++++++++++- ext/rszr/errors.c | 4 +- ext/rszr/image.c | 109 +++++++++++++++++++++++++++---------- lib/rszr.rb | 3 +- lib/rszr/color.rb | 66 ++-------------------- lib/rszr/color/base.rb | 21 +++++++ lib/rszr/color/cmya.rb | 29 ++++++++++ lib/rszr/color/gradient.rb | 40 ++++++++++++++ lib/rszr/color/point.rb | 29 ++++++++++ lib/rszr/color/rgba.rb | 44 +++++++++++++++ lib/rszr/fill.rb | 21 +++++++ lib/rszr/image.rb | 21 ++++++- 13 files changed, 328 insertions(+), 94 deletions(-) create mode 100644 lib/rszr/color/base.rb create mode 100644 lib/rszr/color/cmya.rb create mode 100644 lib/rszr/color/gradient.rb create mode 100644 lib/rszr/color/point.rb create mode 100644 lib/rszr/color/rgba.rb create mode 100644 lib/rszr/fill.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bcf6ff..0547431 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ * Alpha channel control * Background initialization * Image blending +* Rectangle and image fills +* Color gradients ## Rszr 1.2.0 (Mar 11, 2022) diff --git a/README.md b/README.md index 8e50ac9..36ec4c5 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,36 @@ 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) +``` + +### Watermarking + +```ruby +# load logo +logo = Rszr::Image.load('logo.png') + +# blend it onto the image at position (10, 10) +image.blend(logo, 10, 10) +``` + ### Filters Filters also support bang! and non-bang methods. @@ -170,8 +200,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 1d60a6f..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) @@ -112,21 +116,18 @@ static VALUE rszr_image__format_set(VALUE self, VALUE rb_format) } -static void rszr_image_color_set(VALUE rb_Color) +static void rszr_image_color_set(VALUE rb_color) { - VALUE rb_cColorBase; int r, g, b, a; - rb_cColorBase = rb_path2class("Rszr::Color::Base"); - - if(!rb_obj_is_kind_of(rb_Color, rb_cColorBase) || RBASIC_CLASS(rb_Color) == rb_cColorBase) { + 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)); + 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); @@ -539,9 +540,57 @@ static VALUE rszr_image__blend(VALUE self, VALUE other, VALUE rb_merge_alpha, VA } -static VALUE rszr_image_rectangle_bang(VALUE self, VALUE rb_color, VALUE rb_x, VALUE rb_y, VALUE rb_w, VALUE rb_h) +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); @@ -553,25 +602,21 @@ static VALUE rszr_image_rectangle_bang(VALUE self, VALUE rb_color, VALUE rb_x, V 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); - rszr_image_color_set(rb_color); - imlib_image_fill_rectangle(x, y, w, h); - - return self; -} - -static VALUE rszr_image_fill_bang(VALUE self, VALUE rb_color) -{ - rszr_image_handle * handle; - - Data_Get_Struct(self, rszr_image_handle, handle); - - imlib_context_set_image(handle->image); - rszr_image_color_set(rb_color); - imlib_image_fill_rectangle(0, 0, imlib_image_get_width(), imlib_image_get_height()); + 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; } @@ -614,6 +659,12 @@ 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 @@ -626,15 +677,13 @@ void Init_rszr_image() rb_define_method(cImage, "filter!", rszr_image_filter_bang, 1); rb_define_method(cImage, "flop!", rszr_image_flop_bang, 0); rb_define_method(cImage, "flip!", rszr_image_flip_bang, 0); - rb_define_method(cImage, "rectangle!", rszr_image_rectangle_bang, 5); - rb_define_method(cImage, "fill!", rszr_image_fill_bang, 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, "_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); @@ -643,6 +692,8 @@ void Init_rszr_image() 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 3f8f866..32afb8d 100644 --- a/lib/rszr/color.rb +++ b/lib/rszr/color.rb @@ -1,66 +1,12 @@ +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 Base - attr_reader :alpha - 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 | alpha.to_i - alpha ? i : i >> 8 - end - - def to_hex(rgb: false) - "%0#{rgb ? 6 : 8}x" % to_i(rgb: rgb) - end - end - - 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 - Transparent = RGBA.new(0, 0, 0, 0) White = RGBA.new(255,255,255) Black = RGBA.new(0, 0, 0) diff --git a/lib/rszr/color/base.rb b/lib/rszr/color/base.rb new file mode 100644 index 0000000..1d320a5 --- /dev/null +++ b/lib/rszr/color/base.rb @@ -0,0 +1,21 @@ +module Rszr + module Color + + class Base + attr_reader :alpha + + def rgba + [red, green, blue, alpha] + end + + def cmya + [cyan, magenta, yellow, alpha] + 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..705d358 --- /dev/null +++ b/lib/rszr/color/gradient.rb @@ -0,0 +1,40 @@ +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) + 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..3fc22e7 --- /dev/null +++ b/lib/rszr/color/rgba.rb @@ -0,0 +1,44 @@ +module Rszr + module Color + + class << self + def rgba(red, green, blue, alpha = 255) + RGBA.new(red, green, blue, alpha) + 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 | alpha.to_i + alpha ? i : i >> 8 + end + + def to_hex(rgb: false) + "%0#{rgb ? 6 : 8}x" % to_i(rgb: rgb) + 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 3f1c535..45f3b78 100644 --- a/lib/rszr/image.rb +++ b/lib/rszr/image.rb @@ -148,17 +148,36 @@ def gamma(*args, **opts) dup.gamma!(*args, **opts) end + # bang? def blend(image, 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, 0, 0, image.width, image.height) 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 descend from Rszr::Color::Base' if background && !(background.class < Color::Base) + 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 From bef9a4f4a162cc8e54bd76a99e687403c6539cf3 Mon Sep 17 00:00:00 2001 From: Matthias Grosser Date: Tue, 29 Mar 2022 13:03:57 +0200 Subject: [PATCH 4/4] Hex color code support --- CHANGELOG.md | 8 +++-- README.md | 70 +++++++++++++++++++++++++++++++++++--- lib/rszr/color/base.rb | 4 +++ lib/rszr/color/gradient.rb | 2 ++ lib/rszr/color/rgba.rb | 18 ++++++++-- lib/rszr/image.rb | 9 +++-- spec/rszr_spec.rb | 4 +-- 7 files changed, 101 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0547431..d15eeb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,15 @@ ## Rszr 1.3.0 (unreleased) -* Fix saving without extension (@mantas) * Alpha channel control * Background initialization -* Image blending +* 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) ## Rszr 1.2.0 (Mar 11, 2022) diff --git a/README.md b/README.md index 36ec4c5..59bea1e 100644 --- a/README.md +++ b/README.md @@ -60,9 +60,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 @@ -141,14 +142,75 @@ end image.rectangle!(gradient.to_fill(45), 100, 100, 300, 300) ``` -### Watermarking +### 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) +image.blend!(logo, 10, 10) + +# blending modes: +# - copy (default) +# - add +# - subtract +# - reshade +image.blend(logo, 10, 10, mode: :subtract) ``` ### Filters diff --git a/lib/rszr/color/base.rb b/lib/rszr/color/base.rb index 1d320a5..bc0d070 100644 --- a/lib/rszr/color/base.rb +++ b/lib/rszr/color/base.rb @@ -12,6 +12,10 @@ 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 diff --git a/lib/rszr/color/gradient.rb b/lib/rszr/color/gradient.rb index 705d358..d022b94 100644 --- a/lib/rszr/color/gradient.rb +++ b/lib/rszr/color/gradient.rb @@ -21,6 +21,8 @@ def <<(position, red = nil, green = nil, blue= nil, alpha = 255) 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 diff --git a/lib/rszr/color/rgba.rb b/lib/rszr/color/rgba.rb index 3fc22e7..9b41245 100644 --- a/lib/rszr/color/rgba.rb +++ b/lib/rszr/color/rgba.rb @@ -5,6 +5,18 @@ 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 @@ -30,12 +42,12 @@ def yellow end def to_i(alpha: true) - i = red.to_i << 24 | green.to_i << 16 | blue.to_i << 8 | alpha.to_i + 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(rgb: false) - "%0#{rgb ? 6 : 8}x" % to_i(rgb: rgb) + def to_hex(alpha: true) + "#%0#{alpha ? 8 : 6}x" % to_i(alpha: alpha) end end diff --git a/lib/rszr/image.rb b/lib/rszr/image.rb index 45f3b78..287c56d 100644 --- a/lib/rszr/image.rb +++ b/lib/rszr/image.rb @@ -148,10 +148,13 @@ def gamma(*args, **opts) dup.gamma!(*args, **opts) end - # bang? - def blend(image, mode: :copy) + 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, 0, 0, image.width, image.height) + _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) diff --git a/spec/rszr_spec.rb b/spec/rszr_spec.rb index e5db69d..da288a8 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