add stroke text, stretch symbols

This commit is contained in:
GenevensiS
2026-01-13 04:56:02 +01:00
parent 775bdac085
commit 6ca8abae1a
28 changed files with 766 additions and 245 deletions
+17 -1
View File
@@ -53,11 +53,15 @@ void sharp_resample_and_clip(const Image& img_in, Image& img_out, wxRect rect, i
* rect = rectangle to draw in (a rectangle somewhere around pos)
* stretch = amount to stretch in the direction of the text after drawing
*/
void draw_resampled_text(DC& dc, const RealPoint& pos, const RealRect& rect, double stretch, Radians angle, Color color, const String& text, int blur_radius = 0, int repeat = 1);
void draw_resampled_text(DC& dc, const String& text, const RealPoint& pos, const RealRect& rect, Radians angle, Color color, int blur_radius = 0, Color stroke_color = Color(0,0,0), int stroke_radius = 0, double stretch = 1.0);
// scaling factor to use when drawing resampled text
extern const int text_scaling;
// Downsamples the red channel of the input image to the alpha channel of the output image
// img_in must be text_scaling times as large as img_out
void downsample_to_alpha(Bitmap& bmp_in, Image& img_out);
// ----------------------------------------------------------------------------- : Image rotation
/// Rotates an image counter clockwise
@@ -92,6 +96,18 @@ void saturate(Image& image, double amount);
/// Invert the colors in an image
void invert(Image& img);
// Blur the alpha of a pixel
Byte blur_pixel_alpha(Byte* in, int x, int y, int width, int height, int center_weight = 2);
// Blur the alpha channel of an image
void blur_image_alpha(Image& img, int center_weight = 2);
// Thicken the alpha of a pixel
Byte thicken_pixel_alpha_3x3(std::vector<Byte> in, int i, int x, int y, int width, int height);
Byte thicken_pixel_alpha_5x5(std::vector<Byte> in, int i, int x, int y, int width, int height);
Byte thicken_pixel_alpha_7x7(std::vector<Byte> 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);
// ----------------------------------------------------------------------------- : Combining
/// Ways in which images can be combined, similair to what Photoshop supports
+291
View File
@@ -9,6 +9,9 @@
#include <util/prec.hpp>
#include <gfx/gfx.hpp>
#include <util/error.hpp>
#if defined(__WXMSW__) && wxUSE_WXDIB
#include <wx/msw/dib.h>
#endif
// ----------------------------------------------------------------------------- : Saturation
@@ -71,6 +74,294 @@ void invert(Image& img) {
}
}
// ----------------------------------------------------------------------------- : Blurring
Byte blur_pixel_alpha(Byte* in, int x, int y, int width, int height, int center_weight) {
return ( center_weight * in[0] + // center
(x == 0 ? in[0] : in[-1]) + // left
(y == 0 ? in[0] : in[-width]) + // up
(x == width - 1 ? in[0] : in[1]) + // right
(y == height - 1 ? in[0] : in[width]) // down
) / (4 + center_weight);
}
void blur_image_alpha(Image& img, int center_weight) {
if (!img.HasAlpha()) return;
int width = img.GetWidth(), height = img.GetHeight();
Byte* data = img.GetAlpha();
for (int y = 0 ; y < height ; ++y) {
for (int x = 0 ; x < width ; ++x) {
*data = blur_pixel_alpha(data, x, y, width, height, center_weight);
++data;
}
}
}
// ----------------------------------------------------------------------------- : Thickening
Byte thicken_pixel_alpha_7x7(std::vector<unsigned char> in, int i, int x, int y, int width, int height) {
Byte result = in[i];
Byte diag = in[i];
Byte corner = in[i];
if (x > 2) {
if (y > 1) corner = max(corner, in[i -3 -2*width]);
if (y > 0) diag = max(diag , in[i -3 -1*width]);
result = max(result, in[i -3 ]);
if (y < height - 1) diag = max(diag , in[i -3 +1*width]);
if (y < height - 2) corner = max(corner, in[i -3 +2*width]);
}
if (x > 1) {
if (y > 2) corner = max(corner, in[i -2 -3*width]);
if (y > 1) result = max(result, in[i -2 -2*width]);
if (y > 0) result = max(result, in[i -2 -1*width]);
result = max(result, in[i -2 ]);
if (y < height - 1) result = max(result, in[i -2 +1*width]);
if (y < height - 2) result = max(result, in[i -2 +2*width]);
if (y < height - 3) corner = max(corner, in[i -2 +3*width]);
}
if (x > 0) {
if (y > 2) diag = max(diag , in[i -1 -3*width]);
if (y > 1) result = max(result, in[i -1 -2*width]);
if (y > 0) result = max(result, in[i -1 -1*width]);
result = max(result, in[i -1 ]);
if (y < height - 1) result = max(result, in[i -1 +1*width]);
if (y < height - 2) result = max(result, in[i -1 +2*width]);
if (y < height - 3) diag = max(diag , in[i -1 +3*width]);
}
if (y > 2) result = max(result, in[i -3*width]);
if (y > 1) result = max(result, in[i -2*width]);
if (y > 0) result = max(result, in[i -1*width]);
if (y < height - 1) result = max(result, in[i +1*width]);
if (y < height - 2) result = max(result, in[i +2*width]);
if (y < height - 3) result = max(result, in[i +3*width]);
if (x < width - 1) {
if (y > 2) diag = max(diag , in[i +1 -3*width]);
if (y > 1) result = max(result, in[i +1 -2*width]);
if (y > 0) result = max(result, in[i +1 -1*width]);
result = max(result, in[i +1 ]);
if (y < height - 1) result = max(result, in[i +1 +1*width]);
if (y < height - 2) result = max(result, in[i +1 +2*width]);
if (y < height - 3) diag = max(diag , in[i +1 +3*width]);
}
if (x < width - 2) {
if (y > 2) corner = max(corner, in[i +2 -3*width]);
if (y > 1) result = max(result, in[i +2 -2*width]);
if (y > 0) result = max(result, in[i +2 -1*width]);
result = max(result, in[i +2 ]);
if (y < height - 1) result = max(result, in[i +2 +1*width]);
if (y < height - 2) result = max(result, in[i +2 +2*width]);
if (y < height - 3) corner = max(corner, in[i +2 +3*width]);
}
if (x < width - 3) {
if (y > 1) corner = max(corner, in[i +3 -2*width]);
if (y > 0) diag = max(diag , in[i +3 -1*width]);
result = max(result, in[i +3 ]);
if (y < height - 1) diag = max(diag , in[i +3 +1*width]);
if (y < height - 2) corner = max(corner, in[i +3 +2*width]);
}
if (diag > result) result = (Byte)((4 * (int)diag + (int)result) / 5);
if (corner > result) result = (Byte)((2 * (int)corner + 3 * (int)result) / 5);
return result;
}
Byte thicken_pixel_alpha_5x5(std::vector<unsigned char> in, int i, int x, int y, int width, int height) {
Byte result = in[i];
Byte diag = in[i];
if (x > 1) {
if (y > 0) diag = max(diag , in[i -2 -1*width]);
result = max(result, in[i -2 ]);
if (y < height - 1) diag = max(diag , in[i -2 +1*width]);
}
if (x > 0) {
if (y > 1) diag = max(diag , in[i -1 -2*width]);
if (y > 0) result = max(result, in[i -1 -1*width]);
result = max(result, in[i -1 ]);
if (y < height - 1) result = max(result, in[i -1 +1*width]);
if (y < height - 2) diag = max(diag , in[i -1 +2*width]);
}
if (y > 1) result = max(result, in[i -2*width]);
if (y > 0) result = max(result, in[i -1*width]);
if (y < height - 1) result = max(result, in[i +1*width]);
if (y < height - 2) result = max(result, in[i +2*width]);
if (x < width - 1) {
if (y > 1) diag = max(diag , in[i +1 -2*width]);
if (y > 0) result = max(result, in[i +1 -1*width]);
result = max(result, in[i +1 ]);
if (y < height - 1) result = max(result, in[i +1 +1*width]);
if (y < height - 2) diag = max(diag , in[i +1 +2*width]);
}
if (x < width - 2) {
if (y > 0) diag = max(diag , in[i +2 -1*width]);
result = max(result, in[i +2 ]);
if (y < height - 1) diag = max(diag , in[i +2 +1*width]);
}
if (diag > result) result = (Byte)((3 * (int)diag + (int)result) / 4);
return result;
}
Byte thicken_pixel_alpha_3x3(std::vector<unsigned char> in, int i, int x, int y, int width, int height) {
Byte result = in[i];
Byte diag = in[i];
if (x > 0) {
if (y > 0) diag = max(diag, in[i -1 -width]);
result = max(result, in[i -1 ]);
if (y < height - 1) diag = max(diag, in[i -1 +width]);
}
if (y > 0) result = max(result, in[i -width]);
if (y < height - 1) result = max(result, in[i +width]);
if (x < width - 1) {
if (y > 0) diag = max(diag, in[i +1 -width]);
result = max(result, in[i +1 ]);
if (y < height - 1) diag = max(diag, in[i +1 +width]);
}
if (diag > result) result = (Byte)((3*(int)diag + 2*(int)result) / 5);
return result;
}
void thicken_image_alpha(Image& img, int radius) {
if (!img.HasAlpha()) return;
int width = img.GetWidth(), height = img.GetHeight();
Byte* data = img.GetAlpha();
std::vector<Byte> copy(data, data + width * height);
int i = 0;
for (int y = 0 ; y < height ; ++y) {
for (int x = 0 ; x < width ; ++x) {
*data = radius == 1 ? thicken_pixel_alpha_3x3(copy, i, x, y, width, height) :
radius == 2 ? thicken_pixel_alpha_5x5(copy, i, x, y, width, height) :
thicken_pixel_alpha_7x7(copy, i, x, y, width, height) ;
++data;
++i;
}
}
}
// ----------------------------------------------------------------------------- : Resampled text
void downsample_to_alpha(Bitmap& bmp_in, Image& img_out) {
Byte* temp = nullptr;
#if defined(__WXMSW__) && wxUSE_WXDIB
wxDIB img_in(bmp_in);
if (!img_in.IsOk()) return;
// if text_scaling = 4, then the line always is dword aligned, so we need no adjusting
// we created a bitmap with depth 24, so that is what we should have here
if (img_in.GetDepth() != 24) throw InternalError(_("DIB has wrong bit depth"));
#else
Image img_in = bmp_in.ConvertToImage();
#endif
Byte* in = img_in.GetData();
Byte* out = img_in.GetData();
// scale in the x direction, this overwrites parts of the input image
if (img_in.GetWidth() == img_out.GetWidth() * text_scaling) {
// no stretching
int count = img_out.GetWidth() * img_in.GetHeight();
for (int i = 0 ; i < count ; ++i) {
int total = 0;
for (int j = 0 ; j < text_scaling ; ++j) {
total += in[3 * (j + text_scaling * i)];
}
out[i] = total / text_scaling;
}
} else {
// resample to buffer
temp = new Byte[img_out.GetWidth() * img_in.GetHeight()];
out = temp;
// custom stretch, see resample_image.cpp
const int shift = 32-12-8; // => max size = 4096, max alpha = 255
int w1 = img_in.GetWidth(), w2 = img_out.GetWidth(), h = img_in.GetHeight();
int out_fact = (w2 << shift) / w1; // how much to output for 256 input = 1 pixel
int out_rest = (w2 << shift) % w1;
// make the image 'bolder' to compensate for compressing it
int mul = 128 + min(256, 128*w1/(text_scaling*w2));
for (int y = 0 ; y < h ; ++y) {
int in_rem = out_fact + out_rest;
for (int x = 0 ; x < w2 ; ++x) {
int out_rem = 1 << shift;
int tot = 0;
while (out_rem >= in_rem) {
// eat a whole input pixel
tot += *in * in_rem;
out_rem -= in_rem;
in_rem = out_fact;
in += 3;
}
if (out_rem > 0) {
// eat a partial input pixel
tot += *in * out_rem;
in_rem -= out_rem;
}
// store
*out = top(((tot >> shift) * mul) >> 8);
out += 1;
}
}
in = temp;
}
// now scale in the y direction, and write to the output alpha
img_out.InitAlpha();
int line_size_in = img_out.GetWidth();
#if defined(__WXMSW__) && wxUSE_WXDIB
// DIBs are upside down
out = img_out.GetAlpha() + (img_out.GetHeight() - 1) * line_size_in;
int line_size_out = -line_size_in;
#else
out = img_out.GetAlpha();
int line_size_out = line_size_in;
#endif
int h = img_out.GetHeight();
if (img_in.GetHeight() == h * text_scaling) {
// no stretching
for (int y = 0 ; y < h ; ++y) {
for (int x = 0 ; x < line_size_in ; ++x) {
int total = 0;
for (int j = 0 ; j < text_scaling ; ++j) {
total += in[x + line_size_in * (j + text_scaling * y)];
}
out[x + line_size_out * y] = total / text_scaling;
}
}
} else {
const int shift = 32-12-8; // => max size = 4096, max alpha = 255
int h1 = img_in.GetHeight(), w = img_out.GetWidth();
int out_fact = (h << shift) / h1; // how much to output for 256 input = 1 pixel
int out_rest = (h << shift) % h1;
int mul = 128 + min(256, 128*h1/(text_scaling*h));
for (int x = 0 ; x < w ; ++x) {
int in_rem = out_fact + out_rest;
for (int y = 0 ; y < h ; ++y) {
int out_rem = 1 << shift;
int tot = 0;
while (out_rem >= in_rem) {
// eat a whole input pixel
tot += *in * in_rem;
out_rem -= in_rem;
in_rem = out_fact;
in += line_size_in;
}
if (out_rem > 0) {
// eat a partial input pixel
tot += *in * out_rem;
in_rem -= out_rem;
}
// store
*out = top(((tot >> shift) * mul) >> 8);
out += line_size_out;
}
in = in - h1 * line_size_in + 1;
out = out - h * line_size_out + 1;
}
}
delete[] temp;
}
// ----------------------------------------------------------------------------- : Coloring symbol images
RGB recolor(RGB x, RGB cr, RGB cg, RGB cb, RGB cw) {
+53 -168
View File
@@ -10,9 +10,6 @@
#include <gfx/gfx.hpp>
#include <util/error.hpp>
#include <gui/util.hpp> // clearDC_black
#if defined(__WXMSW__) && wxUSE_WXDIB
#include <wx/msw/dib.h>
#endif
void blur_image(const Image& img_in, Image& img_out);
@@ -21,189 +18,77 @@ void blur_image(const Image& img_in, Image& img_out);
// scaling factor to use when drawing resampled text
const int text_scaling = 4;
// Downsamples the red channel of the input image to the alpha channel of the output image
// img_in must be text_scaling times as large as img_out
void downsample_to_alpha(Bitmap& bmp_in, Image& img_out) {
Byte* temp = nullptr;
#if defined(__WXMSW__) && wxUSE_WXDIB
wxDIB img_in(bmp_in);
if (!img_in.IsOk()) return;
// if text_scaling = 4, then the line always is dword aligned, so we need no adjusting
// we created a bitmap with depth 24, so that is what we should have here
if (img_in.GetDepth() != 24) throw InternalError(_("DIB has wrong bit depth"));
#else
Image img_in = bmp_in.ConvertToImage();
#endif
Byte* in = img_in.GetData();
Byte* out = img_in.GetData();
// scale in the x direction, this overwrites parts of the input image
if (img_in.GetWidth() == img_out.GetWidth() * text_scaling) {
// no stretching
int count = img_out.GetWidth() * img_in.GetHeight();
for (int i = 0 ; i < count ; ++i) {
int total = 0;
for (int j = 0 ; j < text_scaling ; ++j) {
total += in[3 * (j + text_scaling * i)];
}
out[i] = total / text_scaling;
}
} else {
// resample to buffer
temp = new Byte[img_out.GetWidth() * img_in.GetHeight()];
out = temp;
// custom stretch, see resample_image.cpp
const int shift = 32-12-8; // => max size = 4096, max alpha = 255
int w1 = img_in.GetWidth(), w2 = img_out.GetWidth(), h = img_in.GetHeight();
int out_fact = (w2 << shift) / w1; // how much to output for 256 input = 1 pixel
int out_rest = (w2 << shift) % w1;
// make the image 'bolder' to compensate for compressing it
int mul = 128 + min(256, 128*w1/(text_scaling*w2));
for (int y = 0 ; y < h ; ++y) {
int in_rem = out_fact + out_rest;
for (int x = 0 ; x < w2 ; ++x) {
int out_rem = 1 << shift;
int tot = 0;
while (out_rem >= in_rem) {
// eat a whole input pixel
tot += *in * in_rem;
out_rem -= in_rem;
in_rem = out_fact;
in += 3;
}
if (out_rem > 0) {
// eat a partial input pixel
tot += *in * out_rem;
in_rem -= out_rem;
}
// store
*out = top(((tot >> shift) * mul) >> 8);
out += 1;
}
}
in = temp;
}
// now scale in the y direction, and write to the output alpha
img_out.InitAlpha();
int line_size_in = img_out.GetWidth();
#if defined(__WXMSW__) && wxUSE_WXDIB
// DIBs are upside down
out = img_out.GetAlpha() + (img_out.GetHeight() - 1) * line_size_in;
int line_size_out = -line_size_in;
#else
out = img_out.GetAlpha();
int line_size_out = line_size_in;
#endif
int h = img_out.GetHeight();
if (img_in.GetHeight() == h * text_scaling) {
// no stretching
for (int y = 0 ; y < h ; ++y) {
for (int x = 0 ; x < line_size_in ; ++x) {
int total = 0;
for (int j = 0 ; j < text_scaling ; ++j) {
total += in[x + line_size_in * (j + text_scaling * y)];
}
out[x + line_size_out * y] = total / text_scaling;
}
}
} else {
const int shift = 32-12-8; // => max size = 4096, max alpha = 255
int h1 = img_in.GetHeight(), w = img_out.GetWidth();
int out_fact = (h << shift) / h1; // how much to output for 256 input = 1 pixel
int out_rest = (h << shift) % h1;
int mul = 128 + min(256, 128*h1/(text_scaling*h));
for (int x = 0 ; x < w ; ++x) {
int in_rem = out_fact + out_rest;
for (int y = 0 ; y < h ; ++y) {
int out_rem = 1 << shift;
int tot = 0;
while (out_rem >= in_rem) {
// eat a whole input pixel
tot += *in * in_rem;
out_rem -= in_rem;
in_rem = out_fact;
in += line_size_in;
}
if (out_rem > 0) {
// eat a partial input pixel
tot += *in * out_rem;
in_rem -= out_rem;
}
// store
*out = top(((tot >> shift) * mul) >> 8);
out += line_size_out;
}
in = in - h1 * line_size_in + 1;
out = out - h * line_size_out + 1;
}
}
delete[] temp;
}
// simple blur
int blur_alpha_pixel(Byte* in, int x, int y, int width, int height) {
return (2 * ( in[0]) + // center
(x == 0 ? in[0] : in[-1]) + // left
(y == 0 ? in[0] : in[-width]) + // up
(x == width - 1 ? in[0] : in[1]) + // right
(y == height - 1 ? in[0] : in[width]) // down
) / 6;
}
// TODO: move me?
void blur_image_alpha(Image& img) {
int width = img.GetWidth(), height = img.GetHeight();
Byte* data = img.GetAlpha();
for (int y = 0 ; y < height ; ++y) {
for (int x = 0 ; x < width ; ++x) {
*data = blur_alpha_pixel(data, x, y, width, height);
++data;
}
}
}
// Draw text by first drawing it using a larger font and then downsampling it
// optionally rotated by an angle
void draw_resampled_text(DC& dc, const RealPoint& pos, const RealRect& rect, double stretch, Radians angle, Color color, const String& text, int blur_radius, int repeat) {
void draw_resampled_text(DC& dc, const String& text, const RealPoint& pos, const RealRect& rect, Radians angle, Color color, int blur_radius, Color stroke_color, int stroke_radius, double stretch) {
// transparent text can be ignored
if (color.Alpha() == 0) return;
// enlarge slightly; some fonts are larger then the GetTextExtent tells us (especially italic fonts)
int w = static_cast<int>(rect.width) + 3 + 2 * blur_radius, h = static_cast<int>(rect.height) + 1 + 2 * blur_radius;
int w = static_cast<int>(rect.width) + 4, h = static_cast<int>(rect.height) + 2;
// determine sub-pixel position
int xi = static_cast<int>(rect.x) - blur_radius / text_scaling,
yi = static_cast<int>(rect.y) - blur_radius / text_scaling;
int xi = static_cast<int>(rect.x),
yi = static_cast<int>(rect.y);
int xsub = static_cast<int>(text_scaling * (pos.x - xi)),
ysub = static_cast<int>(text_scaling * (pos.y - yi));
// draw text
Bitmap buffer(w * text_scaling, h * text_scaling, 24); // should be initialized to black
// draw text as mask (white text on black background)
Bitmap buffer(w * text_scaling, h * text_scaling, 24);
wxMemoryDC mdc;
mdc.SelectObject(buffer);
clearDC_black(mdc);
// now draw the text
mdc.SetFont(dc.GetFont());
mdc.SetTextForeground(*wxWHITE);
mdc.DrawRotatedText(text, xsub, ysub, rad_to_deg(angle));
// get image
mdc.SelectObject(wxNullBitmap);
// step 2. sample down
// downsample
double ca = fabs(cos(angle)), sa = fabs(sin(angle));
w += int(w * (stretch - 1) * ca); // GCC makes annoying conversion warnings if *= is used here.
h += int(h * (stretch - 1) * sa);
Image img_small(w, h, false);
fill_image(img_small, color);
downsample_to_alpha(buffer, img_small);
// multiply alpha
if (color.Alpha() != 255) {
set_alpha(img_small, color.Alpha() / 255.);
}
// blur
for (int i = 0 ; i < blur_radius ; ++i) {
blur_image_alpha(img_small);
}
// step 3. draw to dc
for (int i = 0 ; i < repeat ; ++i) {
dc.DrawBitmap(img_small, xi, yi);
Image img(w, h, false);
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);
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);
}
fill_image(s_img, color);
dc.DrawBitmap(s_img, xi-blur_radius, yi-blur_radius);
}
else {
fill_image(img, color);
dc.DrawBitmap(img, xi, yi);
}
}
// otherwise add stroke effect
else {
int radius = blur_radius + stroke_radius;
Image s_img(w + 2*radius, h + 2*radius);
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);
if (stroke_color != color) {
fill_image(img, color);
if (color.Alpha() != 255) {
set_alpha(img, color.Alpha() / 255.);
}
s_img.Paste(img, radius, radius, wxIMAGE_ALPHA_BLEND_COMPOSE);
}
dc.DrawBitmap(s_img, xi-radius, yi-radius);
}
}