From 8afd412021196b2cbdc2999a287a24cb72bdccbd Mon Sep 17 00:00:00 2001 From: GenevensiS <66968533+G-e-n-e-v-e-n-s-i-S@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:48:17 +0100 Subject: [PATCH] add 'add_stroke_effect' script function --- doc/function/index.txt | 1 + doc/function/stroke_image.txt | 15 ++++++ src/data/symbol_font.cpp | 86 ++-------------------------------- src/gfx/generated_image.cpp | 20 ++++++++ src/gfx/generated_image.hpp | 17 +++++++ src/gfx/gfx.hpp | 3 ++ src/gfx/image_effects.cpp | 46 +++++++++++++++++- src/gfx/resample_text.cpp | 36 ++++---------- src/script/functions/image.cpp | 10 ++++ src/util/rotation.cpp | 5 +- 10 files changed, 128 insertions(+), 111 deletions(-) create mode 100644 doc/function/stroke_image.txt diff --git a/doc/function/index.txt b/doc/function/index.txt index 143f79a3..54cca5ae 100644 --- a/doc/function/index.txt +++ b/doc/function/index.txt @@ -91,6 +91,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:resize_image]] Stretch or squeeze an image to a given height and width. | [[fun:enlarge]] Enlarge an image by putting a border around it. +| [[fun:add_stroke_effect]] Add a stroke effect around an image. | [[fun:add_bleed_edge]] Add a crude print bleed edge around an image. | [[fun:crop]] Crop an image, giving only a small subset of it. | [[fun:flip_horizontal]] Flip an image horizontally. diff --git a/doc/function/stroke_image.txt b/doc/function/stroke_image.txt new file mode 100644 index 00000000..581a38c9 --- /dev/null +++ b/doc/function/stroke_image.txt @@ -0,0 +1,15 @@ +Function: add_stroke_effect + +--Usage-- +> add_stroke_effect(input: image, radius: number, blur: number, color: color) + +Add a stroke effect around an image. + +--Parameters-- +! Parameter Type Description +| @input@ [[type:image]] Image that needs a stroke effect +| @radius@ [[type:int]] Size of the stroke, in pixels +| @blur@ [[type:int]] Size of the blur of the stroke, in pixels. Optional, defaults to 0 +| @color@ [[type:color]] Color of the stroke +| @include_image@ [[type:boolean]] Draw the original image in the middle of the stroke effect. Optional, defaults to true + diff --git a/src/data/symbol_font.cpp b/src/data/symbol_font.cpp index c1cf6641..0da3715b 100644 --- a/src/data/symbol_font.cpp +++ b/src/data/symbol_font.cpp @@ -301,96 +301,20 @@ void SymbolFont::draw(RotatedDC& dc, RealRect rect, double scale, const SymbolFo RealPoint bmp_pos = align_in_rect(font.alignment(), bmp_size, sym_rect); // 2. draw potential stroke or shadow if (font.hasStroke()) { - // add margin - Image img = bmp.ConvertToImage(); - if (!img.HasAlpha()) set_alpha(img, 0); int blur_radius = lround(font.stroke_blur() * s_scale); int stroke_radius = lround(font.stroke_radius() * s_scale); - int margin = blur_radius + stroke_radius; - int s_width = img.GetWidth() + 2 * margin, s_height = img.GetHeight() + 2 * margin; - int x_end = s_width - margin; - int y_end = s_height - margin; - wxImage s_img(s_width, s_height, false); - s_img.InitAlpha(); - // convert to stroke color - Byte* s_data = s_img.GetData(); - Byte* s_alpha = s_img.GetAlpha(), *alpha = img.GetAlpha(); - Color color = font.stroke_color(); - unsigned char r = color.Red(); - unsigned char g = color.Green(); - unsigned char b = color.Blue(); - unsigned char a = color.Alpha(); - for (int y = 0 ; y < s_height ; ++y) { - for (int x = 0 ; x < s_width ; ++x) { - s_data[0] = r; - s_data[1] = g; - s_data[2] = b; - s_data += 3; - if (margin <= x && x < x_end && margin <= y && y < y_end) { - s_alpha[0] = alpha[0] * a / 255; - alpha += 1; - } - else { - s_alpha[0] = 0; - } - s_alpha += 1; - } - } - // add stroke effect - for (int i = 0 ; i < stroke_radius ; ++i) { - thicken_image_alpha(s_img, 1); - } - // add blur - for (int i = 0 ; i < blur_radius ; ++i) { - blur_image_alpha(s_img, 3); - } - // draw + Image s_img = make_stroke_image(bmp.ConvertToImage(), font.stroke_color(), stroke_radius, blur_radius); RealSize s_size = dc.trInvS(RealSize(s_img)); RealPoint s_pos(bmp_pos.x - (s_size.width - bmp_size.width)/2, bmp_pos.y - (s_size.height - bmp_size.height)/2); dc.DrawImage(s_img, s_pos); } else if (font.hasShadow()) { - // add margin - Image img = bmp.ConvertToImage(); - if (!img.HasAlpha()) set_alpha(img, 0); - int margin = lround(font.shadow_blur() * s_scale); - int s_width = img.GetWidth() + 2 * margin, s_height = img.GetHeight() + 2 * margin; - int x_end = s_width - margin; - int y_end = s_height - margin; - wxImage s_img(s_width, s_height, false); - s_img.InitAlpha(); - // convert to shadow color - Byte* s_data = s_img.GetData(); - Byte* s_alpha = s_img.GetAlpha(), *alpha = img.GetAlpha(); - Color color = font.shadow_color(); - unsigned char r = color.Red(); - unsigned char g = color.Green(); - unsigned char b = color.Blue(); - unsigned char a = color.Alpha(); - for (int y = 0 ; y < s_height ; ++y) { - for (int x = 0 ; x < s_width ; ++x) { - s_data[0] = r; - s_data[1] = g; - s_data[2] = b; - s_data += 3; - if (margin <= x && x < x_end && margin <= y && y < y_end) { - s_alpha[0] = alpha[0] * a / 255; - alpha += 1; - } - else { - s_alpha[0] = 0; - } - s_alpha += 1; - } - } - // add blur - for (int i = 0 ; i < margin ; ++i) { - blur_image_alpha(s_img, 3); - } - // draw + int blur_radius = lround(font.shadow_blur() * s_scale); + Image s_img = make_stroke_image(bmp.ConvertToImage(), font.shadow_color(), 0, blur_radius); RealSize s_size = dc.trInvS(RealSize(s_img)); RealPoint s_pos(bmp_pos.x - (s_size.width - bmp_size.width)/2, bmp_pos.y - (s_size.height - bmp_size.height)/2); - dc.DrawImage(s_img, s_pos + RealPoint(font.shadow_displacement_x(), font.shadow_displacement_y()) * scale); + RealSize s_displacement = dc.trInvS(RealSize(font.shadow_displacement_x, font.shadow_displacement_y) * s_scale); + dc.DrawImage(s_img, s_pos + s_displacement); } bmps.push_back(std::move(bmp)); bmp_sizes.push_back(bmp_size); diff --git a/src/gfx/generated_image.cpp b/src/gfx/generated_image.cpp index d0c2d01c..7d1b639a 100644 --- a/src/gfx/generated_image.cpp +++ b/src/gfx/generated_image.cpp @@ -320,6 +320,26 @@ bool ResizeImage::operator == (const GeneratedImage& that) const { && height == that2->height; } +// ----------------------------------------------------------------------------- : StrokeImage + +Image StrokeImage::generate(const Options& opt) { + // create enlarged image + Image base_img = base_image->generate(opt); + Image s_img = make_stroke_image(base_img, color, radius, blur); + if (include_image) { + int offset = radius + blur; + s_img.Paste(base_img, offset, offset, wxIMAGE_ALPHA_BLEND_COMPOSE); + } + return s_img; +} +bool StrokeImage::operator == (const GeneratedImage& that) const { + const StrokeImage* that2 = dynamic_cast(&that); + return that2 && *base_image == *that2->base_image + && radius == that2->radius + && blur == that2->blur + && color == that2->color; +} + // ----------------------------------------------------------------------------- : BleedEdgedImage Image BleedEdgedImage::generate(const Options& opt) { diff --git a/src/gfx/generated_image.hpp b/src/gfx/generated_image.hpp index d6e49567..d37c3555 100644 --- a/src/gfx/generated_image.hpp +++ b/src/gfx/generated_image.hpp @@ -318,6 +318,23 @@ private: double offset_x, offset_y; }; +// ----------------------------------------------------------------------------- : StrokeImage + +/// Create a stroke effect that goes around an image +class StrokeImage : public GeneratedImage { +public: + inline StrokeImage(const GeneratedImageP& base_image, int radius, int blur, Color color, bool include_image) + : base_image(base_image), radius(radius), blur(blur), color(color), include_image(include_image) + {} + Image generate(const Options& opt) override; + bool operator == (const GeneratedImage& that) const override; +private: + GeneratedImageP base_image; + int radius, blur; + Color color; + bool include_image; +}; + // ----------------------------------------------------------------------------- : BleedEdgedImage /// Add a crude bleed edge to an image diff --git a/src/gfx/gfx.hpp b/src/gfx/gfx.hpp index fbcdbbb9..91333093 100644 --- a/src/gfx/gfx.hpp +++ b/src/gfx/gfx.hpp @@ -107,6 +107,9 @@ Byte thicken_pixel_alpha_5x5(std::vector in, int i, int x, int y, int widt Byte thicken_pixel_alpha_7x7(std::vector in, int i, int x, int y, int width, int height); // Thicken the alpha channel of an image void thicken_image_alpha(Image& img, int radius); + +// Create a stroke effect that goes around an image +Image make_stroke_image(Image& img, Color stroke_color, int stroke_radius, int blur_radius = 0); // ----------------------------------------------------------------------------- : Combining diff --git a/src/gfx/image_effects.cpp b/src/gfx/image_effects.cpp index acd2155f..950368f2 100644 --- a/src/gfx/image_effects.cpp +++ b/src/gfx/image_effects.cpp @@ -360,7 +360,51 @@ void downsample_to_alpha(Bitmap& bmp_in, Image& img_out) { } delete[] temp; -} +} + +Image make_stroke_image(Image& img, Color stroke_color, int stroke_radius, int blur_radius) { + stroke_radius = max(0,min(100,stroke_radius)); + blur_radius = max(0,min(100,blur_radius)); + if (!img.HasAlpha()) set_alpha(img, 255); + int margin = blur_radius + stroke_radius; + int s_width = img.GetWidth() + 2 * margin, s_height = img.GetHeight() + 2 * margin; + int x_end = s_width - margin; + int y_end = s_height - margin; + wxImage s_img(s_width, s_height, false); + s_img.InitAlpha(); + // convert to stroke color + Byte* s_data = s_img.GetData(); + Byte* s_alpha = s_img.GetAlpha(), *alpha = img.GetAlpha(); + unsigned char r = stroke_color.Red(); + unsigned char g = stroke_color.Green(); + unsigned char b = stroke_color.Blue(); + unsigned char a = stroke_color.Alpha(); + for (int y = 0 ; y < s_height ; ++y) { + for (int x = 0 ; x < s_width ; ++x) { + s_data[0] = r; + s_data[1] = g; + s_data[2] = b; + s_data += 3; + if (margin <= x && x < x_end && margin <= y && y < y_end) { + s_alpha[0] = alpha[0] * a / 255; + alpha += 1; + } + else { + s_alpha[0] = 0; + } + s_alpha += 1; + } + } + // add stroke effect + for (int i = 0 ; i < stroke_radius ; ++i) { + thicken_image_alpha(s_img, 1); + } + // add blur + for (int i = 0 ; i < blur_radius ; ++i) { + blur_image_alpha(s_img, 3); + } + return s_img; +} // ----------------------------------------------------------------------------- : Coloring symbol images diff --git a/src/gfx/resample_text.cpp b/src/gfx/resample_text.cpp index 19004505..148e8b33 100644 --- a/src/gfx/resample_text.cpp +++ b/src/gfx/resample_text.cpp @@ -47,40 +47,22 @@ void draw_resampled_text(DC& dc, const String& text, const RealPoint& pos, const downsample_to_alpha(buffer, img); // if there is no stroke effect, just add blur and draw if (stroke_radius == 0) { - if (color.Alpha() != 255) { - set_alpha(img, color.Alpha() / 255.); - } - if (blur_radius > 0) { - Image s_img(w + 2*blur_radius, h + 2*blur_radius, false); - set_alpha(s_img, 0); - s_img.Paste(img, blur_radius, blur_radius, wxIMAGE_ALPHA_BLEND_COMPOSE); - for (int i = 0 ; i < blur_radius ; ++i) { - blur_image_alpha(s_img, 3); + if (blur_radius == 0) { + if (color.Alpha() != 255) { + set_alpha(img, color.Alpha() / 255.); } - fill_image(s_img, color); - dc.DrawBitmap(s_img, xi-blur_radius, yi-blur_radius); + fill_image(img, color); + dc.DrawBitmap(img, xi, yi); } else { - fill_image(img, color); - dc.DrawBitmap(img, xi, yi); + Image s_img = make_stroke_image(img, color, 0, blur_radius); + dc.DrawBitmap(s_img, xi-blur_radius, yi-blur_radius); } } - // otherwise add stroke effect + // otherwise add stroke effect, add copy of text on top, draw else { int radius = blur_radius + stroke_radius; - Image s_img(w + 2*radius, h + 2*radius, false); - set_alpha(s_img, 0); - s_img.Paste(img, radius, radius, wxIMAGE_ALPHA_BLEND_COMPOSE); - for (int i = 0 ; i < stroke_radius ; ++i) { - thicken_image_alpha(s_img, 1); - } - for (int i = 0 ; i < blur_radius ; ++i) { - blur_image_alpha(s_img, 3); - } - if (stroke_color.Alpha() != 255) { - set_alpha(s_img, stroke_color.Alpha() / 255.); - } - fill_image(s_img, stroke_color); + Image s_img = make_stroke_image(img, stroke_color, stroke_radius, blur_radius); if (stroke_color != color) { fill_image(img, color); if (color.Alpha() != 255) { diff --git a/src/script/functions/image.cpp b/src/script/functions/image.cpp index 91f16249..1c6344c4 100644 --- a/src/script/functions/image.cpp +++ b/src/script/functions/image.cpp @@ -172,6 +172,15 @@ SCRIPT_FUNCTION(enlarge) { return make_intrusive(input, border_size); } +SCRIPT_FUNCTION(add_stroke_effect) { + SCRIPT_PARAM_C(GeneratedImageP, input); + SCRIPT_PARAM(Color, color); + SCRIPT_PARAM(int, radius); + SCRIPT_PARAM_DEFAULT(int, blur, 0); + SCRIPT_PARAM_DEFAULT(bool, include_image, true); + return make_intrusive(input, radius, blur, color, include_image); +} + SCRIPT_FUNCTION(add_bleed_edge) { SCRIPT_PARAM_C(GeneratedImageP, input); SCRIPT_PARAM_DEFAULT(double, horizontal_size, -1.0); @@ -308,6 +317,7 @@ void init_script_image_functions(Context& ctx) { ctx.setVariable(_("invert_image"), script_invert_image); ctx.setVariable(_("recolor_image"), script_recolor_image); ctx.setVariable(_("enlarge"), script_enlarge); + ctx.setVariable(_("add_stroke_effect"),script_add_stroke_effect); ctx.setVariable(_("add_bleed_edge"), script_add_bleed_edge); ctx.setVariable(_("resize_image"), script_resize_image); ctx.setVariable(_("crop"), script_crop); diff --git a/src/util/rotation.cpp b/src/util/rotation.cpp index b8e64ea4..7f01a6e4 100644 --- a/src/util/rotation.cpp +++ b/src/util/rotation.cpp @@ -220,8 +220,9 @@ void RotatedDC::DrawText(const String& text, const RealPoint& pos, Color color, void RotatedDC::DrawTextWithShadowOrStroke(const String& text, const Font& font, const RealPoint& pos, double scale, double stretch) { double s_scale = scale * dc.GetFont().GetPointSize() / text_scaling / 15.; - if (font.hasShadow() && !font.hasStroke()) { - DrawText(text, pos + RealSize(font.shadow_displacement_x, font.shadow_displacement_y) * scale, font.shadow_color, lround(font.shadow_blur * s_scale), Color(0,0,0), 0, stretch); + if (font.hasShadow() && !font.hasStroke()) { + RealSize shadow_displacement = trInvS(RealSize(font.shadow_displacement_x, font.shadow_displacement_y) * s_scale); + DrawText(text, pos + shadow_displacement, font.shadow_color, lround(font.shadow_blur * s_scale), Color(0,0,0), 0, stretch); } DrawText(text, pos, font.color, lround(font.stroke_blur * s_scale), font.stroke_color, lround(font.stroke_radius * s_scale), stretch); }