From 8e792b44cd987c748a8551c14e7c2d0a21a0e07b Mon Sep 17 00:00:00 2001 From: Djack Donovan Date: Sun, 9 Oct 2022 02:09:02 +0200 Subject: [PATCH 1/4] Script: Added resize_image function This allow exporter script (that don't always have control over the size of input images) to do more image operations on input images --- doc/function/index.txt | 1 + doc/function/resize_image.txt | 14 ++++++++++++++ src/gfx/generated_image.cpp | 15 +++++++++++++++ src/gfx/generated_image.hpp | 14 ++++++++++++++ src/script/functions/image.cpp | 8 ++++++++ .../drupal/mse-drupal-modules/highlight.inc | 1 + 6 files changed, 53 insertions(+) create mode 100644 doc/function/resize_image.txt diff --git a/doc/function/index.txt b/doc/function/index.txt index 700fdf7d7..8e8b355b0 100644 --- a/doc/function/index.txt +++ b/doc/function/index.txt @@ -88,6 +88,7 @@ These functions are built into the program, other [[type:function]]s can be defi | [[fun:recolor_image]] Change the colors of an image to match the font color. | [[fun:enlarge]] Enlarge an image by putting a border around it. | [[fun:crop]] Crop an image, giving only a small subset of it. +| [[fun:resize_image]] Resize an image. | [[fun:flip_horizontal]] Flip an image horizontally. | [[fun:flip_vertical]] Flip an image vertically. | [[fun:rotate_image]] Rotate an image. diff --git a/doc/function/resize_image.txt b/doc/function/resize_image.txt new file mode 100644 index 000000000..cb3bcd86d --- /dev/null +++ b/doc/function/resize_image.txt @@ -0,0 +1,14 @@ +Function: resize_image + +DOC_MSE_VERSION: since 0.3.9 + +--Usage-- +> resize_image(input: image) + +Resize an image. + +--Parameters-- +! Parameter Type Description +| @input@ [[type:image]] Image to resize. +| @height@ [[type:double]] Height of the resulting image +| @width@ [[type:double]] Width of the resulting image diff --git a/src/gfx/generated_image.cpp b/src/gfx/generated_image.cpp index 5465c16f0..bd5d56ff1 100644 --- a/src/gfx/generated_image.cpp +++ b/src/gfx/generated_image.cpp @@ -316,6 +316,21 @@ bool CropImage::operator == (const GeneratedImage& that) const { && offset_x == that2->offset_x && offset_y == that2->offset_y; } +// ----------------------------------------------------------------------------- : ResizeImage + +Image ResizeImage::generate(const Options& opt) const { + Image resampled_image((int)width, (int)height, false); + if (!resampled_image.Ok()) + return Image(1, 1); + resample(image->generate(opt), resampled_image); + return resampled_image; +} +bool ResizeImage::operator == (const GeneratedImage& that) const { + const ResizeImage* that2 = dynamic_cast(&that); + return that2 && *image == *that2->image + && width == that2->width && height == that2->height; +} + // ----------------------------------------------------------------------------- : DropShadowImage /// Preform a gaussian blur, from the image in of w*h bytes to out diff --git a/src/gfx/generated_image.hpp b/src/gfx/generated_image.hpp index fd14ccd12..2cc6b27ec 100644 --- a/src/gfx/generated_image.hpp +++ b/src/gfx/generated_image.hpp @@ -302,6 +302,20 @@ class CropImage : public SimpleFilterImage { double offset_x, offset_y; }; +// ----------------------------------------------------------------------------- : ResizeImage + +/// Resize an image +class ResizeImage : public SimpleFilterImage { +public: + inline ResizeImage(const GeneratedImageP& image, double width, double height) + : SimpleFilterImage(image), width(width), height(height) + {} + Image generate(const Options& opt) const override; + bool operator == (const GeneratedImage& that) const override; +private: + double width, height; +}; + // ----------------------------------------------------------------------------- : DropShadowImage /// Add a drop shadow to an image diff --git a/src/script/functions/image.cpp b/src/script/functions/image.cpp index 3e9caf65d..073289d24 100644 --- a/src/script/functions/image.cpp +++ b/src/script/functions/image.cpp @@ -113,6 +113,13 @@ SCRIPT_FUNCTION(crop) { return make_intrusive(input, width, height, offset_x, offset_y); } +SCRIPT_FUNCTION(resize_image) { + SCRIPT_PARAM_C(GeneratedImageP, input); + SCRIPT_PARAM(int, width); + SCRIPT_PARAM(int, height); + return make_intrusive(input, width, height); +} + SCRIPT_FUNCTION(flip_horizontal) { SCRIPT_PARAM_C(GeneratedImageP, input); return make_intrusive(input); @@ -221,6 +228,7 @@ void init_script_image_functions(Context& ctx) { ctx.setVariable(_("recolor_image"), script_recolor_image); ctx.setVariable(_("enlarge"), script_enlarge); ctx.setVariable(_("crop"), script_crop); + ctx.setVariable(_("resize_image"), script_resize_image); ctx.setVariable(_("flip_horizontal"), script_flip_horizontal); ctx.setVariable(_("flip_vertical"), script_flip_vertical); ctx.setVariable(_("rotate"), script_rotate); diff --git a/tools/website/drupal/mse-drupal-modules/highlight.inc b/tools/website/drupal/mse-drupal-modules/highlight.inc index c34c41c41..957db6c90 100644 --- a/tools/website/drupal/mse-drupal-modules/highlight.inc +++ b/tools/website/drupal/mse-drupal-modules/highlight.inc @@ -82,6 +82,7 @@ $built_in_functions = array( 'recolor_image' =>'', 'enlarge' =>'', 'crop' =>'', + 'resize_image' =>'', 'flip_horizontal' =>'', 'flip_vertical' =>'', 'rotate' =>'', From c674a862b563be797f89de25268c0da86507920f Mon Sep 17 00:00:00 2001 From: Djack Donovan Date: Sun, 9 Oct 2022 21:23:52 +0200 Subject: [PATCH 2/4] Script: Added overlay function --- doc/function/index.txt | 1 + doc/function/overlay.txt | 20 ++++++++++++++ doc/function/resize_image.txt | 4 +-- src/gfx/generated_image.cpp | 27 +++++++++++++++++++ src/gfx/generated_image.hpp | 17 ++++++++++++ src/script/functions/image.cpp | 9 +++++++ .../drupal/mse-drupal-modules/highlight.inc | 1 + 7 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 doc/function/overlay.txt diff --git a/doc/function/index.txt b/doc/function/index.txt index 8e8b355b0..a05e1a41f 100644 --- a/doc/function/index.txt +++ b/doc/function/index.txt @@ -88,6 +88,7 @@ These functions are built into the program, other [[type:function]]s can be defi | [[fun:recolor_image]] Change the colors of an image to match the font color. | [[fun:enlarge]] Enlarge an image by putting a border around it. | [[fun:crop]] Crop an image, giving only a small subset of it. +| [[fun:overlay]] Overlay an image over another. | [[fun:resize_image]] Resize an image. | [[fun:flip_horizontal]] Flip an image horizontally. | [[fun:flip_vertical]] Flip an image vertically. diff --git a/doc/function/overlay.txt b/doc/function/overlay.txt new file mode 100644 index 000000000..b5bbddfa5 --- /dev/null +++ b/doc/function/overlay.txt @@ -0,0 +1,20 @@ +Function: overlay + +--Usage-- +> overlay(image1: image, image2: image, offset_x: x_position_to_overlay_image, offset_y: y_position_to_overlay_image) + +Overlay an image over another. + +--Parameters-- +! Parameter Type Description +| @image1@ [[type:image]] Base image +| @image2@ [[type:image]] Image to overlay over the base image +| @offset_x@ [[type:double]] Offset of the overlayed image, horizontally +| @offset_y@ [[type:double]] Offset of the overlayed image, vertically + +--Examples-- +> combine_blend(image1: "image1.png", image2: "image2.png", combine: "add") == [[Image]] +>>> combine_blend(image1: "image1.png", image2: "image2.png", combine: "add") == "image_combine_blend.png" + +--See also-- +| [[fun:crop]] Crop an image, giving only a small subset of it. diff --git a/doc/function/resize_image.txt b/doc/function/resize_image.txt index cb3bcd86d..1b8a696c4 100644 --- a/doc/function/resize_image.txt +++ b/doc/function/resize_image.txt @@ -3,9 +3,9 @@ Function: resize_image DOC_MSE_VERSION: since 0.3.9 --Usage-- -> resize_image(input: image) +> resize_image(input: image, height: height_of_new_image, width: width_of_new_image) -Resize an image. +Resize an image using bilinear filtering. --Parameters-- ! Parameter Type Description diff --git a/src/gfx/generated_image.cpp b/src/gfx/generated_image.cpp index bd5d56ff1..d56467d4a 100644 --- a/src/gfx/generated_image.cpp +++ b/src/gfx/generated_image.cpp @@ -146,6 +146,33 @@ bool CombineBlendImage::operator == (const GeneratedImage& that) const { && image_combine == that2->image_combine; } +// ----------------------------------------------------------------------------- : OverlayImage + +Image OverlayImage::generate(const Options& opt) const { + Image img = image1->generate(opt); + Image img2 = image2->generate(opt); + if (img.GetWidth() < img2.GetWidth() + offset_x || img.GetHeight() < img2.GetHeight() + offset_y) + throw ScriptError(_("Overlayed image is out of bounds")); + + int off_x = offset_x, off_y = offset_y; + int w = img.GetWidth(), h = img.GetHeight(); // original image size + int ow = img2.GetWidth(), oh = img2.GetHeight(); // overlay image size + Byte* data = img.GetData(), *data2 = img2.GetData(); + + for (int y = 0; y < oh; ++y) { + memcpy(data + 3 * (off_x + (y + off_y) * w), data2 + 3 * y * ow, 3 * ow); // copy a line + } + return img; +} +ImageCombine OverlayImage::combine() const { + return image1->combine(); +} +bool OverlayImage::operator == (const GeneratedImage& that) const { + const OverlayImage* that2 = dynamic_cast(&that); + return that2 && image1 == that2->image1 && image2 == that2->image2 + && offset_x == that2->offset_x && offset_y == that2->offset_y; +} + // ----------------------------------------------------------------------------- : SetMaskImage Image SetMaskImage::generate(const Options& opt) const { diff --git a/src/gfx/generated_image.hpp b/src/gfx/generated_image.hpp index 2cc6b27ec..5d1138501 100644 --- a/src/gfx/generated_image.hpp +++ b/src/gfx/generated_image.hpp @@ -147,6 +147,23 @@ class CombineBlendImage : public GeneratedImage { ImageCombine image_combine; }; +// ----------------------------------------------------------------------------- : OverlayImage + +/// Overlay an image over another +class OverlayImage : public GeneratedImage { +public: + inline OverlayImage(const GeneratedImageP& image1, const GeneratedImageP& image2, double offset_x, double offset_y) + : image1(image1), image2(image2), offset_x(offset_x), offset_y(offset_y) + {} + Image generate(const Options& opt) const override; + ImageCombine combine() const override; + bool operator == (const GeneratedImage& that) const override; + bool local() const override { return image1->local() && image2->local(); } +private: + GeneratedImageP image1, image2; + double offset_x, offset_y; +}; + // ----------------------------------------------------------------------------- : SetMaskImage /// Change the alpha channel of an image diff --git a/src/script/functions/image.cpp b/src/script/functions/image.cpp index 073289d24..bdecb7c75 100644 --- a/src/script/functions/image.cpp +++ b/src/script/functions/image.cpp @@ -54,6 +54,14 @@ SCRIPT_FUNCTION(combine_blend) { return make_intrusive(image1, image2, image_combine); } +SCRIPT_FUNCTION(overlay) { + SCRIPT_PARAM(GeneratedImageP, image1); + SCRIPT_PARAM(GeneratedImageP, image2); + SCRIPT_PARAM(int, offset_x); + SCRIPT_PARAM(int, offset_y); + return make_intrusive(image1, image2, offset_x, offset_y); +} + SCRIPT_FUNCTION(set_mask) { SCRIPT_PARAM(GeneratedImageP, image); SCRIPT_PARAM(GeneratedImageP, mask); @@ -220,6 +228,7 @@ void init_script_image_functions(Context& ctx) { ctx.setVariable(_("linear_blend"), script_linear_blend); ctx.setVariable(_("masked_blend"), script_masked_blend); ctx.setVariable(_("combine_blend"), script_combine_blend); + ctx.setVariable(_("overlay"), script_overlay); ctx.setVariable(_("set_mask"), script_set_mask); ctx.setVariable(_("set_alpha"), script_set_alpha); ctx.setVariable(_("set_combine"), script_set_combine); diff --git a/tools/website/drupal/mse-drupal-modules/highlight.inc b/tools/website/drupal/mse-drupal-modules/highlight.inc index 957db6c90..90a69df58 100644 --- a/tools/website/drupal/mse-drupal-modules/highlight.inc +++ b/tools/website/drupal/mse-drupal-modules/highlight.inc @@ -74,6 +74,7 @@ $built_in_functions = array( 'linear_blend' =>'', 'masked_blend' =>'', 'combine_blend' =>'', + 'overlay' =>'', 'set_mask' =>'', 'set_alpha' =>'', 'set_combine' =>'', From 7b577e5d369e0189b6b9ded34b03ca491e382828 Mon Sep 17 00:00:00 2001 From: Djack Donovan Date: Tue, 11 Oct 2022 20:29:16 +0200 Subject: [PATCH 3/4] overlay fct now takes image alpha into account --- doc/function/overlay.txt | 2 +- src/gfx/generated_image.cpp | 41 ++++++++++++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/doc/function/overlay.txt b/doc/function/overlay.txt index b5bbddfa5..d8a00de50 100644 --- a/doc/function/overlay.txt +++ b/doc/function/overlay.txt @@ -3,7 +3,7 @@ Function: overlay --Usage-- > overlay(image1: image, image2: image, offset_x: x_position_to_overlay_image, offset_y: y_position_to_overlay_image) -Overlay an image over another. +Overlay an image over another, taking the second image's alpha channel into account. --Parameters-- ! Parameter Type Description diff --git a/src/gfx/generated_image.cpp b/src/gfx/generated_image.cpp index d56467d4a..dd7c7d2c2 100644 --- a/src/gfx/generated_image.cpp +++ b/src/gfx/generated_image.cpp @@ -155,13 +155,44 @@ Image OverlayImage::generate(const Options& opt) const { throw ScriptError(_("Overlayed image is out of bounds")); int off_x = offset_x, off_y = offset_y; - int w = img.GetWidth(), h = img.GetHeight(); // original image size - int ow = img2.GetWidth(), oh = img2.GetHeight(); // overlay image size - Byte* data = img.GetData(), *data2 = img2.GetData(); + int w1 = img.GetWidth(), h1 = img.GetHeight(); // original image size + int w2 = img2.GetWidth(), h2 = img2.GetHeight(); // overlay image size + Byte* data1 = img.GetData(), *data2 = img2.GetData(); + Byte* alpha1 = img.GetAlpha(), *alpha2 = img2.GetAlpha(); + + // if image2 has alpha, take it into account + if (alpha2) { + for (int x = 0; x < w2; ++x) { + for (int y = 0; y < h2; ++y) { + Byte a = 255 - alpha2[x + y * w2]; + for (int b = 0; b < 3; ++b) { // each pixel is 3 bytes + Byte& d1 = data1[((y + off_y) * w1 + x + off_x) * 3 + b]; + Byte d2 = data2[(y * w2 + x) * 3 + b]; + d1 = (d1 * a + d2 * (255 - a)) / 255; // copied from mask_blend() + } + } + } - for (int y = 0; y < oh; ++y) { - memcpy(data + 3 * (off_x + (y + off_y) * w), data2 + 3 * y * ow, 3 * ow); // copy a line + if (alpha1) { + for (int x = 0; x < w2; ++x) { + for (int y = 0; y < h2; ++y) { + int idx1 = (y + off_y) * w1 + x + off_x; + alpha1[idx1] = max(alpha1[idx1], alpha2[x + y * w2]); + } + } + } + // otherwise, we can copy the data directly + } else { + for (int y = 0; y < h2; ++y) { + memcpy(data1 + 3 * ((y + off_y) * w1 + off_x), data2 + 3 * y * w2, 3 * w2); // copy a line + } + if (alpha1) { + for (int y = 0; y < h2; ++y) { + memset(alpha1 + (y + off_y) * w1 + off_x, w2, 255); + } + } } + return img; } ImageCombine OverlayImage::combine() const { From 9f0d8819d66b9f8c8481dada455337b1282eeaa0 Mon Sep 17 00:00:00 2001 From: Djack Donovan Date: Wed, 12 Oct 2022 17:09:19 +0200 Subject: [PATCH 4/4] resize_image can take an optional resize mode argument --- doc/function/resize_image.txt | 1 + doc/type/resize_mode.txt | 17 +++++++++++++++++ src/gfx/generated_image.cpp | 17 +++++++++++------ src/gfx/generated_image.hpp | 5 +++-- src/script/functions/image.cpp | 15 +++++++++++---- 5 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 doc/type/resize_mode.txt diff --git a/doc/function/resize_image.txt b/doc/function/resize_image.txt index 1b8a696c4..289e74d98 100644 --- a/doc/function/resize_image.txt +++ b/doc/function/resize_image.txt @@ -12,3 +12,4 @@ Resize an image using bilinear filtering. | @input@ [[type:image]] Image to resize. | @height@ [[type:double]] Height of the resulting image | @width@ [[type:double]] Width of the resulting image +| @mode@ [[type:resize_mode]] Resize mode to use (optional), default is nearest diff --git a/doc/type/resize_mode.txt b/doc/type/resize_mode.txt new file mode 100644 index 000000000..97cc01503 --- /dev/null +++ b/doc/type/resize_mode.txt @@ -0,0 +1,17 @@ +Enumeration: resize mode type + +This specifies how an image is to be resized. + +--Script syntax-- +In scripts, resize modes are stored as a string. + +--Possible values-- +! Value Description +| @nearest@ No resampling is done, use the nearest pixel value. +| @bilinear@ Pixels are linearly interpolated. +| @bicubic@ Use [[https://en.wikipedia.org/wiki/Bicubic_interpolation|bicubic interpolation]]. +| @box average@ Use surrounding pixels to calculate an average that will be used for new pixels. Typically used when reducing the size of an image. + +--Examples-- +> resize_image(..., width: ..., height: ..., mode: "bilinear") + diff --git a/src/gfx/generated_image.cpp b/src/gfx/generated_image.cpp index dd7c7d2c2..42a578afe 100644 --- a/src/gfx/generated_image.cpp +++ b/src/gfx/generated_image.cpp @@ -376,17 +376,22 @@ bool CropImage::operator == (const GeneratedImage& that) const { // ----------------------------------------------------------------------------- : ResizeImage +IMPLEMENT_REFLECTION_ENUM(wxImageResizeQuality) { + VALUE_N("nearest", wxIMAGE_QUALITY_NEAREST); + VALUE_N("bilinear", wxIMAGE_QUALITY_BILINEAR); + VALUE_N("bicubic", wxIMAGE_QUALITY_BICUBIC); + VALUE_N("box average", wxIMAGE_QUALITY_BOX_AVERAGE); +} + Image ResizeImage::generate(const Options& opt) const { - Image resampled_image((int)width, (int)height, false); - if (!resampled_image.Ok()) - return Image(1, 1); - resample(image->generate(opt), resampled_image); - return resampled_image; + return image->generate(opt).Rescale((int)width, (int)height, resize_quality); } bool ResizeImage::operator == (const GeneratedImage& that) const { const ResizeImage* that2 = dynamic_cast(&that); return that2 && *image == *that2->image - && width == that2->width && height == that2->height; + && width == that2->width + && height == that2->height + && resize_quality == that2->resize_quality; } // ----------------------------------------------------------------------------- : DropShadowImage diff --git a/src/gfx/generated_image.hpp b/src/gfx/generated_image.hpp index 5d1138501..2f04e6dc0 100644 --- a/src/gfx/generated_image.hpp +++ b/src/gfx/generated_image.hpp @@ -324,13 +324,14 @@ class CropImage : public SimpleFilterImage { /// Resize an image class ResizeImage : public SimpleFilterImage { public: - inline ResizeImage(const GeneratedImageP& image, double width, double height) - : SimpleFilterImage(image), width(width), height(height) + inline ResizeImage(const GeneratedImageP& image, double width, double height, wxImageResizeQuality resize_quality) + : SimpleFilterImage(image), width(width), height(height), resize_quality(resize_quality) {} Image generate(const Options& opt) const override; bool operator == (const GeneratedImage& that) const override; private: double width, height; + wxImageResizeQuality resize_quality; }; // ----------------------------------------------------------------------------- : DropShadowImage diff --git a/src/script/functions/image.cpp b/src/script/functions/image.cpp index bdecb7c75..0dc3cf3b2 100644 --- a/src/script/functions/image.cpp +++ b/src/script/functions/image.cpp @@ -20,6 +20,7 @@ #include void parse_enum(const String&, ImageCombine& out); +void parse_enum(const String&, wxImageResizeQuality& out); // ----------------------------------------------------------------------------- : Utility @@ -122,10 +123,16 @@ SCRIPT_FUNCTION(crop) { } SCRIPT_FUNCTION(resize_image) { - SCRIPT_PARAM_C(GeneratedImageP, input); - SCRIPT_PARAM(int, width); - SCRIPT_PARAM(int, height); - return make_intrusive(input, width, height); + SCRIPT_PARAM_C(GeneratedImageP, input); + SCRIPT_PARAM(int, width); + SCRIPT_PARAM(int, height); + wxImageResizeQuality resize_quality; + SCRIPT_OPTIONAL_PARAM(String, mode) { + parse_enum(mode, resize_quality); + } else { + resize_quality = wxIMAGE_QUALITY_NEAREST; + } + return make_intrusive(input, width, height, resize_quality); } SCRIPT_FUNCTION(flip_horizontal) {