diff --git a/doc/function/index.txt b/doc/function/index.txt index 700fdf7d7..a05e1a41f 100644 --- a/doc/function/index.txt +++ b/doc/function/index.txt @@ -88,6 +88,8 @@ 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. | [[fun:rotate_image]] Rotate an image. diff --git a/doc/function/overlay.txt b/doc/function/overlay.txt new file mode 100644 index 000000000..d8a00de50 --- /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, taking the second image's alpha channel into account. + +--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 new file mode 100644 index 000000000..289e74d98 --- /dev/null +++ b/doc/function/resize_image.txt @@ -0,0 +1,15 @@ +Function: resize_image + +DOC_MSE_VERSION: since 0.3.9 + +--Usage-- +> resize_image(input: image, height: height_of_new_image, width: width_of_new_image) + +Resize an image using bilinear filtering. + +--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 +| @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 5465c16f0..42a578afe 100644 --- a/src/gfx/generated_image.cpp +++ b/src/gfx/generated_image.cpp @@ -146,6 +146,64 @@ 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 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() + } + } + } + + 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 { + 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 { @@ -316,6 +374,26 @@ bool CropImage::operator == (const GeneratedImage& that) const { && offset_x == that2->offset_x && offset_y == that2->offset_y; } +// ----------------------------------------------------------------------------- : 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 { + 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 + && resize_quality == that2->resize_quality; +} + // ----------------------------------------------------------------------------- : 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..2f04e6dc0 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 @@ -302,6 +319,21 @@ 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, 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 /// Add a drop shadow to an image diff --git a/src/script/functions/image.cpp b/src/script/functions/image.cpp index 3e9caf65d..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 @@ -54,6 +55,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); @@ -113,6 +122,19 @@ 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); + 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) { SCRIPT_PARAM_C(GeneratedImageP, input); return make_intrusive(input); @@ -213,6 +235,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); @@ -221,6 +244,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..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' =>'', @@ -82,6 +83,7 @@ $built_in_functions = array( 'recolor_image' =>'', 'enlarge' =>'', 'crop' =>'', + 'resize_image' =>'', 'flip_horizontal' =>'', 'flip_vertical' =>'', 'rotate' =>'',