GDI+ 在Delphi程序的应用 -- 文字描边与阴影扩展

    自从文章《GDI+ 在Delphi程序的应用 -- 可调节的文字阴影特效》发表后,不少人问我怎样实现文字描边。由于我只是个业余编程爱好者,加上文化底蕴差,只要涉及算法和编程理论方面的东西,我就无能为力了,所以直到目前,我也不知道具体的描边算法是怎样的(网上搜索过N次,也没找到答案,可能这方面的东西是要卖钱的)。

    因问得人多了,有时我也思索和研究一下,总算找了个方法可以实现,虽然同专业的图像软件(如PhotoShop)文字描边效果相比差强人意,但可以凑合凑合,作为研究心得,将代码贴在这里备查。

 

    在《GDI+ 在Delphi程序的应用 -- 可调节的文字阴影特效》一文的内容的基础上,对文字阴影效果代码进行了改进和扩充,扩充的功能有2点:一是由原来只能产生黑色阴影扩充为任意颜色阴影;二是可以对阴影进行扩展。有了这2个功能,利用阴影效果也就可以进行文字描边了,推而广之,也可实现图像的描边。下面是具体的代码内容:

 

  1. // 备份图像。Data: GDI+位图数据,32位ARGB格式; Dest: 备份目标; Color: 阴影颜色
  2. procedure BackImage(Data: TBitmapData; Dest: Pointer; Color: TARGB);
  3. asm
  4.     push    esi
  5.     push    edi
  6.     mov     esi, [eax + 16]   // esi = Data.Scan0
  7.     mov     edi, edx          // esi = Dest
  8.     mov     edx, ecx          // edx = Color & 0xffffff
  9.     and     edx, 0FFFFFFh
  10.     mov     ecx, [eax]        // ecx = Data.Height * Data.Width
  11.     imul    ecx, [eax + 4]
  12.     cld
  13.   @Loop:                       // for (; ecx >= 0; ecx --)
  14.     or      [esi], edx
  15.     movsd                      //   *edi++ = *esi++ & 0xff000000 | edx
  16.     loop    @Loop
  17.     pop     edi
  18.     pop     esi
  19. end;
  20. // 扩展。Data: GDI+位图数据,32位ARGB格式; Source: 复制的源
  21. // ExpMatrix: 卷积矩阵; MatrixSize:矩阵大小
  22. procedure MakeExpand(Data: TBitmapData; Source, ExpMatrix: Pointer;
  23.     MatrixSize: LongWord);
  24. var
  25.   Radius, mSize, rSize: LongWord;
  26.   x, y: LongWord;
  27.   Width, Height: Integer;
  28.   Matrix: Pointer;
  29.   Stride: LongWord;
  30. asm
  31.     push    esi
  32.     push    edi
  33.     push    ebx
  34.     mov     esi, edx          // esi = Source
  35.     mov     edi, [eax + 16]   // edi = Data.Scan0 + 3 (Alpha byte)
  36.     add     edi, 3
  37.     add     ecx, 3
  38.     mov     Matrix, ecx       // Matrix = ExpMatrix + 3 (Alpha byte)
  39.     mov     ecx, MatrixSize
  40.     mov     edx, ecx
  41.     dec     ecx
  42.     mov     ebx, [eax]
  43.     sub     ebx, ecx
  44.     mov     Width, ebx        // Width = Data.Width - (MatrixSize - 1)
  45.     mov     ebx, [eax + 4]
  46.     sub     ebx, ecx
  47.     mov     Height, ebx       // Height = Data.Height - (MatrixSize - 1)
  48.     shr     ecx, 1
  49.     mov     Radius, ecx       // Radius = MatrixSize / 2
  50.     mov     eax, [eax + 8]
  51.     mov     Stride, eax
  52.     mov     mSize, eax
  53.     shl     edx, 2
  54.     sub     mSize, edx        // mSize = Data.Stride - MatrixSize * 4
  55.     add     eax, 4
  56.     imul    eax, ecx
  57.     add     eax, 3
  58.     add     esi, eax          // esi = esi + (Data.Stride * Radius + Radius * 4 + 3)
  59.     shl     ecx, 3
  60.     mov     rSize, ecx        // rSize = Radius * 2 * 4
  61.     mov     y, 0              // for (y = 0; y < Height; y ++)
  62.   @yLoop:                     // {
  63.     mov     x, 0              //   for (x = 0; x < Width; x ++)
  64.   @xLoop:                     //   {
  65.     test    [esi], 0ffh       //     if (*esi != 0)
  66.     jz      @NextPixel        //     {
  67.     test    [esi 4], 0ffh
  68.     jz      @001
  69.     test    [esi + 4], 0ffh
  70.     jz      @001
  71.     mov     ebx, Stride
  72.     test    [esi + ebx], 0ffh
  73.     jz      @001
  74.     neg     ebx
  75.     test    [esi + ebx], 0ffh
  76.     jnz     @NextPixel
  77.   @001:
  78.     push    edi               //       Save(edi)
  79.     mov     ebx, Matrix       //       ebx = Matrix
  80.     mov     edx, MatrixSize   //       for (I = 0; I < MatrixSize; I ++)
  81.   @Loop3:                     //       {
  82.     mov     ecx, MatrixSize   //         for (J = 0; J <= MatrixSize; J ++)
  83.   @Loop4:                     //         {
  84.     mov      al, [ebx]        //           *edi = max(*ebx, *edi)
  85.     cmp     al, [edi]
  86.     jb      @002
  87.     mov     [edi], al
  88.   @002:
  89.     add     edi, 4            //           edi += 4
  90.     add     ebx, 4            //           ebx += 4
  91.     loop    @Loop4            //         }
  92.     add     edi, mSize        //         edi += mSize
  93.     dec     edx
  94.     jnz     @Loop3            //       }
  95.     pop     edi               //       Reset(edi)
  96.   @NextPixel:                 //     }
  97.     add     edi, 4            //     edi += 4
  98.     add     esi, 4            //     esi += 4
  99.     inc     x
  100.     mov     eax, x
  101.     cmp     eax, Width
  102.     jl      @xLoop            //   }
  103.     add     esi, rSize
  104.     add     edi, rSize
  105.     inc     y
  106.     mov     eax, y
  107.     cmp     eax, Height
  108.     jl      @yLoop            // }
  109.     pop     ebx
  110.     pop     edi
  111.     pop     esi
  112. end;
  113. procedure GdipShadow(Data: TBitmapData; Buf: Pointer; Radius: LongWord);
  114. var
  115.   Gauss: array of Integer;
  116.   Q: Double;
  117.   x, y, n, z: Integer;
  118.   p: PInteger;
  119. begin
  120.   // 根据半径计算高斯模糊矩阵
  121.   Q := Radius / 2;
  122.   if Q = 0 then Q := 0.1;
  123.   n := Radius shl 1 + 1;
  124.   SetLength(Gauss, n * n);
  125.   p := @Gauss[0];
  126.   z := 0;
  127.   for x := -Radius to Radius do
  128.     for y := -Radius to Radius do
  129.     begin
  130.       p^ := Round(Exp(-(x * x + y * y) / (2.0 * Q * Q)) / (2.0 * PI * Q * Q) * 1000.0);
  131.       Inc(z, p^);
  132.       Inc(p);
  133.     end;
  134.   MakeShadow(Data, Buf, Gauss, n, z);
  135. end;
  136. procedure GdipBorder(Data: TBitmapData; Buf: Pointer; Expand: LongWord; Color: TARGB);
  137. var
  138.   bmp: TGpBitmap;
  139.   bg: TGpGraphics;
  140.   Data1: TBitmapData;
  141.   Size: Integer;
  142. begin
  143.   Size := Expand shl 1 + 1;
  144.   bmp := TGpBitmap.Create(Size, Size, pf32bppARGB);
  145.   bg := TGpGraphics.Create(bmp);
  146.   try
  147.     // 制造一个直径=Size,消除锯齿后的圆作为描边(或扩展)的位图画笔
  148.     bg.SmoothingMode := smAntiAlias;
  149.     bg.PixelOffsetMode := pmHalf;
  150.     bg.FillEllipse(Brushs[Color], 0, 0, Size, Size);
  151.     Data1 := bmp.LockBits(GpRect(00, Size, Size), [imRead], pf32bppARGB);
  152.     try
  153.       // 用位图画笔扩展图像
  154.       MakeExpand(Data, Buf, Data1.Scan0, Size);
  155.     finally
  156.       bmp.UnlockBits(Data1);
  157.     end;
  158.   finally
  159.     bg.Free;
  160.     bmp.Free;
  161.   end;
  162. end;
  163. procedure DrawShadow(const g: TGpGraphics; const Bitmap: TGpBitmap;
  164.     const layoutRect: TGpRectF; ShadowSize, Distance: LongWord;
  165.     Angle: Single; Color: TARGB; Expand: LongWord);
  166. var
  167.   dr, sr: TGpRectF;
  168.   Data: TBitmapData;
  169.   Buf: Pointer;
  170.   SaveScan0: Pointer;
  171. begin
  172.   Data := Bitmap.LockBits(GpRect(00, Bitmap.Width, Bitmap.Height),
  173.                           [imRead, imWrite], pf32bppARGB);
  174.   GetMem(Buf, Data.Height * Data.Stride);
  175.   try
  176.     BackImage(Data, Buf, Color);
  177.     if Expand > ShadowSize then
  178.       Expand := ShadowSize;
  179.     if Expand <> 0 then            // 处理文字阴影扩展
  180.       if Expand <> ShadowSize then
  181.       begin
  182.         SaveScan0 := Data.Scan0;
  183.         Data.Scan0 := Buf;
  184.         GdipBorder(Data, SaveScan0, Expand, Color);
  185.         Data.Scan0 := SaveScan0;
  186.       end else
  187.         GdipBorder(Data, Buf, Expand, Color);
  188.     if Expand <> ShadowSize then   // 处理文字阴影效果
  189.       GdipShadow(Data, Buf, ShadowSize - Expand);
  190.   finally
  191.     FreeMem(Buf);
  192.     Bitmap.UnlockBits(Data);
  193.   end;
  194.   sr := GpRect(0.00.0, Data.Width, Data.Height);
  195. //  sr := GpRect(0.0, 0.0, layoutRect.Width + ShadowSize * 2 + 2,
  196. //               layoutRect.Height + ShadowSize * 2 + 2);
  197.   dr := GpRect(layoutRect.Point, sr.Size);
  198.   // 根据角度计算阴影位图在目标画布的偏移量
  199.   Offset(dr, Cos(PI * Angle / 180) * Distance - ShadowSize - 1,
  200.          Sin(PI * Angle / 180) * Distance - ShadowSize - 1);
  201.   // 输出阴影位图到目标画布
  202.   g.DrawImage(Bitmap, dr, sr.X, sr.Y, sr.Width, sr.Height, utPixel);
  203. end;
  204. // 计算并输出文字阴影效果
  205. // g: 文字输出的画布; str要输出的文字; font: 字体; layoutRect: 限定的文字输出范围
  206. // ShadowSize: 阴影大小; Distance: 阴影距离;
  207. // Angle: 阴影输出角度(左边平行处为0度。顺时针方向)
  208. // ShadowAlpha: 阴影文字的不透明度; format: 文字输出格式
  209. procedure DrawShadowString(const g: TGpGraphics; const str: WideString;
  210.     const font: TGpFont; const layoutRect: TGpRectF;
  211.     ShadowSize, Distance: LongWord; Angle: Single = 60;
  212.     Color: TARGB = $C0000000; Expand: LongWord = 0;
  213.     const format: TGpStringFormat = nil); overload;
  214. var
  215.   Bmp: TGpBitmap;
  216.   Bg: TGpGraphics;
  217. begin
  218.   // 建立透明的32位ARGB阴影位图,大小为layoutRect长、宽度 + ShadowSize * 2 + 2
  219.   Bmp := TGpBitmap.Create(Round(layoutRect.Width + 0.5) + ShadowSize shl 1 + 2,
  220.                           Round(layoutRect.Height + 0.5) + ShadowSize shl 1 + 2,
  221.                           pf32bppARGB);
  222.   Bg := TGpGraphics.Create(Bmp);
  223.   try
  224.     Bg.TextRenderingHint := thAntiAlias;
  225.     // 以Color不透明度的黑色画刷,在ShadowSize + 1处输出文字到位图画布。
  226.     // 方便黑色以外的阴影颜色替换(直接用Color画,模糊处理后很难看)
  227.     Bg.DrawString(str, font, Brushs[Color and $FF000000],
  228.                   GpRect(ShadowSize + 1, ShadowSize + 1,
  229.                   layoutRect.Width, layoutRect.Height), format);
  230.     DrawShadow(g, Bmp, layoutRect, ShadowSize, Distance, Angle, Color, Expand);
  231.   finally
  232.     Bg.Free;
  233.     Bmp.Free;
  234.   end;
  235. end;
  236. // 计算并输出文字阴影效果,除以输出点origin替代上面布局矩形外,其他参数同上
  237. procedure DrawShadowString(const g: TGpGraphics; const str: WideString;
  238.     const font: TGpFont; const origin: TGpPointF;
  239.     ShadowSize, Distance: LongWord; Angle: Single = 60;
  240.     Color: TARGB = $C0000000; Expand: LongWord = 0;
  241.     const format: TGpStringFormat = nil); overload;
  242. begin
  243.   DrawShadowString(g, str, font, g.MeasureString(str, font, origin, format),
  244.                    ShadowSize, Distance, Angle, Color, Expand, format);
  245. end;

    上面代码中MakeShadow过程的代码在GDI+ 在Delphi程序的应用 -- 可调节的文字阴影特效》一文中,本文没有贴出。由于代码中已经有了较详细的注释,故不再解释。下面贴出测试代码:

  1. procedure TextPaint(g: TGpGraphics);
  2. var
  3.   brush: TGpLinearGradientBrush;
  4.   font: TGpFont;
  5.   fontFamily: TGpFontFamily;
  6.   r: TGpRect;
  7. begin
  8.   fontFamily := TGpFontFamily.Create({'Times New Roman'}'华文行楷');
  9.   font := TGpFont.Create(fontFamily, 55, [fsBold], utPixel);
  10.   r := GpRect(Form1.PaintBox1.ClientRect);
  11.   brush := TGpLinearGradientBrush.Create(r, kcBlue, kcAliceBlue, 90);
  12.   g.FillRectangle(Brush, r);
  13.   DrawShadowString(g, '文字阴影特效', font, GpPoint(10, r.Height / 3), 51060$C00000001);
  14.   DrawShadowString(g, '文字阴影特效', font, GpPoint(10, r.Height / 3), 1060$FFFF00001);
  15. //  DrawShadowString(g, '文字阴影特效', font, GpPoint(10, r.Height / 3), 5, 12, 60, $C0000000, 1);
  16. //  DrawShadowString(g, '文字阴影特效', font, GpPoint(10, r.Height / 3), 2, 3, 60, $FFc00000, 1);
  17.   g.TextRenderingHint := thAntiAlias;
  18.   g.DrawString('文字阴影特效', font, Brushs.White, 10, r.Height / 3);
  19.   font.Free;
  20.   fontFamily.Free;
  21.   Brush.Free;
  22. end;

    以下是测试代码效果图,图一和图二都是文字描边(1个像素的边框)加阴影效果,其中图一没进行阴影扩展,即上面的15行的代码最后一个参数为0,图二是加了1个像素的阴影扩展效果(上述代码的“正宗”输出):

图一

图二

 

    利用改进的阴影效果,不仅可实现文字描边,也可显示类似立体文字的效果(改变显示距离),上面测试代码中,被注释的2句代码输出效果如下:

图三

    至于图像的描边,似乎没有文字的描边效果好,究其原因,主要是图像的轮廓看起来好像是圆润平滑的,其实有很多半影锯齿,在Photoshop中,通过先选区后描边,可能对选区边缘作了处理,所以效果相当好(专业的软件,肯定有很好的算法)。下面是我对一张小图片作的描边处理代码和输出效果图:

  1. // 图像描边
  2. // g: 文字输出的画布; Image: 图像; x, y: 图像输出原点
  3. // BorderWidth: 总的边框宽度; Color: 边框颜色;
  4. // Expand: 边框扩散大小; Attributes: 图像显示属性
  5. procedure DrawImageBorder(const g: TGpGraphics; const Image: TGpImage;
  6.     x, y: Single; BorderWidth: LongWord; Color: TARGB = kcWhite;
  7.     Expand: LongWord = 0const Attributes: TGpImageAttributes = nil);
  8. var
  9.   Bmp: TGpBitmap;
  10.   Bg: TGpGraphics;
  11.   ColorMatrix: TColorMatrix;
  12.   Attr: TGpImageAttributes;
  13.   layoutRect: TGpRectF;
  14. begin
  15.   Bmp := TGpBitmap.Create(Image.Width + BorderWidth shl 1 + 2,
  16.                           Image.Height + BorderWidth shl 1 + 2,
  17.                           pf32bppARGB);
  18.   Bg := TGpGraphics.Create(Bmp);
  19.   Attr := Attributes;
  20.   if Attr = nil then
  21.     Attr := TGpImageAttributes.Create;
  22.   try
  23.     FillChar(ColorMatrix, Sizeof(TColorMatrix), 0);
  24.     ColorMatrix[33] := 1;
  25.     ColorMatrix[44] := 1;
  26.     // 利用颜色矩阵将图像输出为黑色,以便边框颜色替换
  27.     Attr.SetColorMatrix(ColorMatrix);
  28.     layoutRect := GpRect(x, y, Image.Width, Image.Height);
  29.     Bg.DrawImage(Image,
  30.                  GpRect(BorderWidth + 1, BorderWidth + 1, layoutRect.Width, layoutRect.Height),
  31.                  00, layoutRect.Width, layoutRect.Height, utPixel, Attr);
  32.     DrawShadow(g, Bmp, layoutRect, BorderWidth, 00, Color, BorderWidth - Expand);
  33.   finally
  34.     if Attributes <> nil then
  35.       Attr.ClearColorMatrix
  36.     else
  37.       Attr.Free;
  38.     Bg.Free;
  39.     Bmp.Free;
  40.   end;
  41. end;
  42. procedure ImagePaint(g: TGpGraphics);
  43. var
  44.   brush: TGpLinearGradientBrush;
  45.   r: TGpRect;
  46.   Image: TGpImage;
  47.   Attributes: TGpImageAttributes;
  48. begin
  49.   r := GpRect(Form1.PaintBox1.ClientRect);
  50.   brush := TGpLinearGradientBrush.Create(r, kcBlue, kcAliceBlue, 90);
  51.   g.FillRectangle(Brush, r);
  52.   Image := TGpImage.Create('../../Media/Watermark.bmp');
  53.   // 画原图
  54.   g.TranslateTransform(20, r.Height / 3);
  55.   g.DrawImage(Image, 00, Image.Width, Image.Height);
  56.   // 设置图像透明色
  57.   Attributes := TGpImageAttributes.Create;
  58.   Attributes.SetColorKey($ff00ff00$ff00ff00);
  59.   // 画2个像素的描边图
  60.   g.TranslateTransform(Image.Width + 200);
  61.   DrawImageBorder(g, Image, 002, kcWhite, 0, Attributes);
  62.   g.DrawImage(Image, GpRect(0.00, Image.Width, Image.Height),
  63.               0.00.0, Image.Width, Image.Height, utPixel, Attributes);
  64.   // 画5个像素的描边图,其中扩散3像素
  65.   g.TranslateTransform(Image.Width + 200);
  66.   DrawImageBorder(g, Image, 005, kcWhite, 3, Attributes);
  67.   g.DrawImage(Image, GpRect(0.00, Image.Width, Image.Height),
  68.               0.00.0, Image.Width, Image.Height, utPixel, Attributes);
  69.   Attributes.Free;
  70.   Brush.Free;
  71.   Image.Free;
  72. 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,可见扩展速度是不大理想的。今天对代码作了一点修改,对每个象素点都进行了判断,如果是边界像素,则作画笔矩阵填充,否则直接跳过,这样一来,速度应该提高不少(没作测试,增加的代码用红色标出,有兴趣者可以测试)。

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值