自从文章《GDI+ 在Delphi程序的应用 -- 可调节的文字阴影特效》发表后,不少人问我怎样实现文字描边。由于我只是个业余编程爱好者,加上文化底蕴差,只要涉及算法和编程理论方面的东西,我就无能为力了,所以直到目前,我也不知道具体的描边算法是怎样的(网上搜索过N次,也没找到答案,可能这方面的东西是要卖钱的)。
因问得人多了,有时我也思索和研究一下,总算找了个方法可以实现,虽然同专业的图像软件(如PhotoShop)文字描边效果相比差强人意,但可以凑合凑合,作为研究心得,将代码贴在这里备查。
在《GDI+ 在Delphi程序的应用 -- 可调节的文字阴影特效》一文的内容的基础上,对文字阴影效果代码进行了改进和扩充,扩充的功能有2点:一是由原来只能产生黑色阴影扩充为任意颜色阴影;二是可以对阴影进行扩展。有了这2个功能,利用阴影效果也就可以进行文字描边了,推而广之,也可实现图像的描边。下面是具体的代码内容:
- // 备份图像。Data: GDI+位图数据,32位ARGB格式; Dest: 备份目标; Color: 阴影颜色
- procedure BackImage(Data: TBitmapData; Dest: Pointer; Color: TARGB);
- asm
- push esi
- push edi
- mov esi, [eax + 16] // esi = Data.Scan0
- mov edi, edx // esi = Dest
- mov edx, ecx // edx = Color & 0xffffff
- and edx, 0FFFFFFh
- mov ecx, [eax] // ecx = Data.Height * Data.Width
- imul ecx, [eax + 4]
- cld
- @Loop: // for (; ecx >= 0; ecx --)
- or [esi], edx
- movsd // *edi++ = *esi++ & 0xff000000 | edx
- loop @Loop
- pop edi
- pop esi
- end;
- // 扩展。Data: GDI+位图数据,32位ARGB格式; Source: 复制的源
- // ExpMatrix: 卷积矩阵; MatrixSize:矩阵大小
- procedure MakeExpand(Data: TBitmapData; Source, ExpMatrix: Pointer;
- MatrixSize: LongWord);
- var
- Radius, mSize, rSize: LongWord;
- x, y: LongWord;
- Width, Height: Integer;
- Matrix: Pointer;
- Stride: LongWord;
- asm
- push esi
- push edi
- push ebx
- mov esi, edx // esi = Source
- mov edi, [eax + 16] // edi = Data.Scan0 + 3 (Alpha byte)
- add edi, 3
- add ecx, 3
- mov Matrix, ecx // Matrix = ExpMatrix + 3 (Alpha byte)
- mov ecx, MatrixSize
- mov edx, ecx
- dec ecx
- mov ebx, [eax]
- sub ebx, ecx
- mov Width, ebx // Width = Data.Width - (MatrixSize - 1)
- mov ebx, [eax + 4]
- sub ebx, ecx
- mov Height, ebx // Height = Data.Height - (MatrixSize - 1)
- shr ecx, 1
- mov Radius, ecx // Radius = MatrixSize / 2
- mov eax, [eax + 8]
- mov Stride, eax
- mov mSize, eax
- shl edx, 2
- sub mSize, edx // mSize = Data.Stride - MatrixSize * 4
- add eax, 4
- imul eax, ecx
- add eax, 3
- add esi, eax // esi = esi + (Data.Stride * Radius + Radius * 4 + 3)
- shl ecx, 3
- mov rSize, ecx // rSize = Radius * 2 * 4
- mov y, 0 // for (y = 0; y < Height; y ++)
- @yLoop: // {
- mov x, 0 // for (x = 0; x < Width; x ++)
- @xLoop: // {
- test [esi], 0ffh // if (*esi != 0)
- jz @NextPixel // {
- test [esi - 4], 0ffh
- jz @001
- test [esi + 4], 0ffh
- jz @001
- mov ebx, Stride
- test [esi + ebx], 0ffh
- jz @001
- neg ebx
- test [esi + ebx], 0ffh
- jnz @NextPixel
- @001:
- push edi // Save(edi)
- mov ebx, Matrix // ebx = Matrix
- mov edx, MatrixSize // for (I = 0; I < MatrixSize; I ++)
- @Loop3: // {
- mov ecx, MatrixSize // for (J = 0; J <= MatrixSize; J ++)
- @Loop4: // {
- mov al, [ebx] // *edi = max(*ebx, *edi)
- cmp al, [edi]
- jb @002
- mov [edi], al
- @002:
- add edi, 4 // edi += 4
- add ebx, 4 // ebx += 4
- loop @Loop4 // }
- add edi, mSize // edi += mSize
- dec edx
- jnz @Loop3 // }
- pop edi // Reset(edi)
- @NextPixel: // }
- add edi, 4 // edi += 4
- add esi, 4 // esi += 4
- inc x
- mov eax, x
- cmp eax, Width
- jl @xLoop // }
- add esi, rSize
- add edi, rSize
- inc y
- mov eax, y
- cmp eax, Height
- jl @yLoop // }
- pop ebx
- pop edi
- pop esi
- end;
- procedure GdipShadow(Data: TBitmapData; Buf: Pointer; Radius: LongWord);
- var
- Gauss: array of Integer;
- Q: Double;
- x, y, n, z: Integer;
- p: PInteger;
- begin
- // 根据半径计算高斯模糊矩阵
- Q := Radius / 2;
- if Q = 0 then Q := 0.1;
- n := Radius shl 1 + 1;
- SetLength(Gauss, n * n);
- p := @Gauss[0];
- z := 0;
- for x := -Radius to Radius do
- for y := -Radius to Radius do
- begin
- p^ := Round(Exp(-(x * x + y * y) / (2.0 * Q * Q)) / (2.0 * PI * Q * Q) * 1000.0);
- Inc(z, p^);
- Inc(p);
- end;
- MakeShadow(Data, Buf, Gauss, n, z);
- end;
- procedure GdipBorder(Data: TBitmapData; Buf: Pointer; Expand: LongWord; Color: TARGB);
- var
- bmp: TGpBitmap;
- bg: TGpGraphics;
- Data1: TBitmapData;
- Size: Integer;
- begin
- Size := Expand shl 1 + 1;
- bmp := TGpBitmap.Create(Size, Size, pf32bppARGB);
- bg := TGpGraphics.Create(bmp);
- try
- // 制造一个直径=Size,消除锯齿后的圆作为描边(或扩展)的位图画笔
- bg.SmoothingMode := smAntiAlias;
- bg.PixelOffsetMode := pmHalf;
- bg.FillEllipse(Brushs[Color], 0, 0, Size, Size);
- Data1 := bmp.LockBits(GpRect(0, 0, Size, Size), [imRead], pf32bppARGB);
- try
- // 用位图画笔扩展图像
- MakeExpand(Data, Buf, Data1.Scan0, Size);
- finally
- bmp.UnlockBits(Data1);
- end;
- finally
- bg.Free;
- bmp.Free;
- end;
- end;
- procedure DrawShadow(const g: TGpGraphics; const Bitmap: TGpBitmap;
- const layoutRect: TGpRectF; ShadowSize, Distance: LongWord;
- Angle: Single; Color: TARGB; Expand: LongWord);
- var
- dr, sr: TGpRectF;
- Data: TBitmapData;
- Buf: Pointer;
- SaveScan0: Pointer;
- begin
- Data := Bitmap.LockBits(GpRect(0, 0, Bitmap.Width, Bitmap.Height),
- [imRead, imWrite], pf32bppARGB);
- GetMem(Buf, Data.Height * Data.Stride);
- try
- BackImage(Data, Buf, Color);
- if Expand > ShadowSize then
- Expand := ShadowSize;
- if Expand <> 0 then // 处理文字阴影扩展
- if Expand <> ShadowSize then
- begin
- SaveScan0 := Data.Scan0;
- Data.Scan0 := Buf;
- GdipBorder(Data, SaveScan0, Expand, Color);
- Data.Scan0 := SaveScan0;
- end else
- GdipBorder(Data, Buf, Expand, Color);
- if Expand <> ShadowSize then // 处理文字阴影效果
- GdipShadow(Data, Buf, ShadowSize - Expand);
- finally
- FreeMem(Buf);
- Bitmap.UnlockBits(Data);
- end;
- sr := GpRect(0.0, 0.0, Data.Width, Data.Height);
- // sr := GpRect(0.0, 0.0, layoutRect.Width + ShadowSize * 2 + 2,
- // layoutRect.Height + ShadowSize * 2 + 2);
- dr := GpRect(layoutRect.Point, sr.Size);
- // 根据角度计算阴影位图在目标画布的偏移量
- Offset(dr, Cos(PI * Angle / 180) * Distance - ShadowSize - 1,
- Sin(PI * Angle / 180) * Distance - ShadowSize - 1);
- // 输出阴影位图到目标画布
- g.DrawImage(Bitmap, dr, sr.X, sr.Y, sr.Width, sr.Height, utPixel);
- end;
- // 计算并输出文字阴影效果
- // g: 文字输出的画布; str要输出的文字; font: 字体; layoutRect: 限定的文字输出范围
- // ShadowSize: 阴影大小; Distance: 阴影距离;
- // Angle: 阴影输出角度(左边平行处为0度。顺时针方向)
- // ShadowAlpha: 阴影文字的不透明度; format: 文字输出格式
- procedure DrawShadowString(const g: TGpGraphics; const str: WideString;
- const font: TGpFont; const layoutRect: TGpRectF;
- ShadowSize, Distance: LongWord; Angle: Single = 60;
- Color: TARGB = $C0000000; Expand: LongWord = 0;
- const format: TGpStringFormat = nil); overload;
- var
- Bmp: TGpBitmap;
- Bg: TGpGraphics;
- begin
- // 建立透明的32位ARGB阴影位图,大小为layoutRect长、宽度 + ShadowSize * 2 + 2
- Bmp := TGpBitmap.Create(Round(layoutRect.Width + 0.5) + ShadowSize shl 1 + 2,
- Round(layoutRect.Height + 0.5) + ShadowSize shl 1 + 2,
- pf32bppARGB);
- Bg := TGpGraphics.Create(Bmp);
- try
- Bg.TextRenderingHint := thAntiAlias;
- // 以Color不透明度的黑色画刷,在ShadowSize + 1处输出文字到位图画布。
- // 方便黑色以外的阴影颜色替换(直接用Color画,模糊处理后很难看)
- Bg.DrawString(str, font, Brushs[Color and $FF000000],
- GpRect(ShadowSize + 1, ShadowSize + 1,
- layoutRect.Width, layoutRect.Height), format);
- DrawShadow(g, Bmp, layoutRect, ShadowSize, Distance, Angle, Color, Expand);
- finally
- Bg.Free;
- Bmp.Free;
- end;
- end;
- // 计算并输出文字阴影效果,除以输出点origin替代上面布局矩形外,其他参数同上
- procedure DrawShadowString(const g: TGpGraphics; const str: WideString;
- const font: TGpFont; const origin: TGpPointF;
- ShadowSize, Distance: LongWord; Angle: Single = 60;
- Color: TARGB = $C0000000; Expand: LongWord = 0;
- const format: TGpStringFormat = nil); overload;
- begin
- DrawShadowString(g, str, font, g.MeasureString(str, font, origin, format),
- ShadowSize, Distance, Angle, Color, Expand, format);
- end;
上面代码中MakeShadow过程的代码在《GDI+ 在Delphi程序的应用 -- 可调节的文字阴影特效》一文中,本文没有贴出。由于代码中已经有了较详细的注释,故不再解释。下面贴出测试代码:
- procedure TextPaint(g: TGpGraphics);
- var
- brush: TGpLinearGradientBrush;
- font: TGpFont;
- fontFamily: TGpFontFamily;
- r: TGpRect;
- begin
- fontFamily := TGpFontFamily.Create({'Times New Roman'}'华文行楷');
- font := TGpFont.Create(fontFamily, 55, [fsBold], utPixel);
- r := GpRect(Form1.PaintBox1.ClientRect);
- brush := TGpLinearGradientBrush.Create(r, kcBlue, kcAliceBlue, 90);
- g.FillRectangle(Brush, r);
- DrawShadowString(g, '文字阴影特效', font, GpPoint(10, r.Height / 3), 5, 10, 60, $C0000000, 1);
- DrawShadowString(g, '文字阴影特效', font, GpPoint(10, r.Height / 3), 1, 0, 60, $FFFF0000, 1);
- // DrawShadowString(g, '文字阴影特效', font, GpPoint(10, r.Height / 3), 5, 12, 60, $C0000000, 1);
- // DrawShadowString(g, '文字阴影特效', font, GpPoint(10, r.Height / 3), 2, 3, 60, $FFc00000, 1);
- g.TextRenderingHint := thAntiAlias;
- g.DrawString('文字阴影特效', font, Brushs.White, 10, r.Height / 3);
- font.Free;
- fontFamily.Free;
- Brush.Free;
- end;
以下是测试代码效果图,图一和图二都是文字描边(1个像素的边框)加阴影效果,其中图一没进行阴影扩展,即上面的15行的代码最后一个参数为0,图二是加了1个像素的阴影扩展效果(上述代码的“正宗”输出):
图一
图二
利用改进的阴影效果,不仅可实现文字描边,也可显示类似立体文字的效果(改变显示距离),上面测试代码中,被注释的2句代码输出效果如下:
图三
至于图像的描边,似乎没有文字的描边效果好,究其原因,主要是图像的轮廓看起来好像是圆润平滑的,其实有很多半影锯齿,在Photoshop中,通过先选区后描边,可能对选区边缘作了处理,所以效果相当好(专业的软件,肯定有很好的算法)。下面是我对一张小图片作的描边处理代码和输出效果图:
- // 图像描边
- // g: 文字输出的画布; Image: 图像; x, y: 图像输出原点
- // BorderWidth: 总的边框宽度; Color: 边框颜色;
- // Expand: 边框扩散大小; Attributes: 图像显示属性
- procedure DrawImageBorder(const g: TGpGraphics; const Image: TGpImage;
- x, y: Single; BorderWidth: LongWord; Color: TARGB = kcWhite;
- Expand: LongWord = 0; const Attributes: TGpImageAttributes = nil);
- var
- Bmp: TGpBitmap;
- Bg: TGpGraphics;
- ColorMatrix: TColorMatrix;
- Attr: TGpImageAttributes;
- layoutRect: TGpRectF;
- begin
- Bmp := TGpBitmap.Create(Image.Width + BorderWidth shl 1 + 2,
- Image.Height + BorderWidth shl 1 + 2,
- pf32bppARGB);
- Bg := TGpGraphics.Create(Bmp);
- Attr := Attributes;
- if Attr = nil then
- Attr := TGpImageAttributes.Create;
- try
- FillChar(ColorMatrix, Sizeof(TColorMatrix), 0);
- ColorMatrix[3, 3] := 1;
- ColorMatrix[4, 4] := 1;
- // 利用颜色矩阵将图像输出为黑色,以便边框颜色替换
- Attr.SetColorMatrix(ColorMatrix);
- layoutRect := GpRect(x, y, Image.Width, Image.Height);
- Bg.DrawImage(Image,
- GpRect(BorderWidth + 1, BorderWidth + 1, layoutRect.Width, layoutRect.Height),
- 0, 0, layoutRect.Width, layoutRect.Height, utPixel, Attr);
- DrawShadow(g, Bmp, layoutRect, BorderWidth, 0, 0, Color, BorderWidth - Expand);
- finally
- if Attributes <> nil then
- Attr.ClearColorMatrix
- else
- Attr.Free;
- Bg.Free;
- Bmp.Free;
- end;
- end;
- procedure ImagePaint(g: TGpGraphics);
- var
- brush: TGpLinearGradientBrush;
- r: TGpRect;
- Image: TGpImage;
- Attributes: TGpImageAttributes;
- begin
- r := GpRect(Form1.PaintBox1.ClientRect);
- brush := TGpLinearGradientBrush.Create(r, kcBlue, kcAliceBlue, 90);
- g.FillRectangle(Brush, r);
- Image := TGpImage.Create('../../Media/Watermark.bmp');
- // 画原图
- g.TranslateTransform(20, r.Height / 3);
- g.DrawImage(Image, 0, 0, Image.Width, Image.Height);
- // 设置图像透明色
- Attributes := TGpImageAttributes.Create;
- Attributes.SetColorKey($ff00ff00, $ff00ff00);
- // 画2个像素的描边图
- g.TranslateTransform(Image.Width + 20, 0);
- DrawImageBorder(g, Image, 0, 0, 2, kcWhite, 0, Attributes);
- g.DrawImage(Image, GpRect(0.0, 0, Image.Width, Image.Height),
- 0.0, 0.0, Image.Width, Image.Height, utPixel, Attributes);
- // 画5个像素的描边图,其中扩散3像素
- g.TranslateTransform(Image.Width + 20, 0);
- DrawImageBorder(g, Image, 0, 0, 5, kcWhite, 3, Attributes);
- g.DrawImage(Image, GpRect(0.0, 0, Image.Width, Image.Height),
- 0.0, 0.0, Image.Width, Image.Height, utPixel, Attributes);
- Attributes.Free;
- Brush.Free;
- Image.Free;
- end;
图四
上面的效果图中,左边是原图,中间是2个像素的描边图,右边是5个像素的描边图,其中有3像素的模糊扩散。从图中可以看出,我以$ff00ff00为透明色处理图像四个角后,在中间和右边的描边图中,还是很明显的看到四个角有很淡的绿色,正是这个原因,在中间图的圆角描边有明显的锯齿。
最后作几点说明:
1、本文纯属业余学习和研究的心得,并非什么正宗的算法;
2、因为本文代码是学习时即兴写的,并非优化代码,而且是以过程形式出现的,有兴趣的朋友可以自己进行优化改进,写成类或者元件更好(由于算法和功能都不是很完善,所以我没写成类的形式);
3、例子中的GDI+版本系本人自己改写的,与网上流通的版本不完全兼容,如需使用本版本,请参照《GDI+ for VCL基础 -- GDI+ 与 VCL 》一文的下载地址,并请留意后面的修改说明。
4、如有好的建议,请来信:maozefa@hotmail.com
更新(2008-8-5 12:50):在MakeExpand过程中,是按图象逐点用位图画笔矩阵填充的,每个像素点都要进行矩阵大小的操作,最小的1像素扩展的矩阵大小为3 * 3,可见扩展速度是不大理想的。今天对代码作了一点修改,对每个象素点都进行了判断,如果是边界像素,则作画笔矩阵填充,否则直接跳过,这样一来,速度应该提高不少(没作测试,增加的代码用红色标出,有兴趣者可以测试)。