`
emowuyi
  • 浏览: 1470503 次
文章分类
社区版块
存档分类
最新评论

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

 
阅读更多

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

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

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

  1. //备份图像。Data:GDI+位图数据,32位ARGB格式;Dest:备份目标;Color:阴影颜色
  2. procedureBackImage(Data:TBitmapData;Dest:Pointer;Color:TARGB);
  3. asm
  4. pushesi
  5. pushedi
  6. movesi,[eax+16]//esi=Data.Scan0
  7. movedi,edx//esi=Dest
  8. movedx,ecx//edx=Color&0xffffff
  9. andedx,0FFFFFFh
  10. movecx,[eax]//ecx=Data.Height*Data.Width
  11. imulecx,[eax+4]
  12. cld
  13. @Loop://for(;ecx>=0;ecx--)
  14. or[esi],edx
  15. movsd//*edi++=*esi++&0xff000000|edx
  16. loop@Loop
  17. popedi
  18. popesi
  19. end;
  20. //扩展。Data:GDI+位图数据,32位ARGB格式;Source:复制的源
  21. //ExpMatrix:卷积矩阵;MatrixSize:矩阵大小
  22. procedureMakeExpand(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. pushesi
  32. pushedi
  33. pushebx
  34. movesi,edx//esi=Source
  35. movedi,[eax+16]//edi=Data.Scan0+3(Alphabyte)
  36. addedi,3
  37. add ecx, 3
  38. movMatrix,ecx//Matrix=ExpMatrix +3(Alphabyte)
  39. movecx,MatrixSize
  40. movedx,ecx
  41. dececx
  42. movebx,[eax]
  43. subebx,ecx
  44. movWidth,ebx//Width=Data.Width-(MatrixSize-1)
  45. movebx,[eax+4]
  46. subebx,ecx
  47. movHeight,ebx//Height=Data.Height-(MatrixSize-1)
  48. shrecx,1
  49. movRadius,ecx//Radius=MatrixSize/2
  50. moveax,[eax+8]
  51. movStride,eax
  52. movmSize,eax
  53. shledx,2
  54. submSize,edx//mSize=Data.Stride-MatrixSize*4
  55. addeax,4
  56. imuleax,ecx
  57. addeax,3
  58. addesi,eax//esi=esi+(Data.Stride*Radius+Radius*4+3)
  59. shlecx,3
  60. movrSize,ecx//rSize=Radius*2*4
  61. movy,0//for(y=0;y<Height;y++)
  62. @yLoop://{
  63. movx,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. movebx,Stride
  72. test[esi+ebx],0ffh
  73. jz@001
  74. negebx
  75. test[esi+ebx],0ffh
  76. jnz@NextPixel
  77. @001:
  78. pushedi//Save(edi)
  79. movebx,Matrix//ebx=Matrix
  80. movedx,MatrixSize//for(I=0;I<MatrixSize;I++)
  81. @Loop3://{
  82. movecx,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. addedi,4//edi+=4
  90. addebx,4//ebx+=4
  91. loop@Loop4//}
  92. addedi,mSize//edi+=mSize
  93. decedx
  94. jnz @Loop3//}
  95. popedi//Reset(edi)
  96. @NextPixel://}
  97. addedi,4//edi+=4
  98. addesi,4//esi+=4
  99. incx
  100. moveax,x
  101. cmpeax,Width
  102. jl@xLoop//}
  103. addesi,rSize
  104. addedi,rSize
  105. incy
  106. moveax,y
  107. cmpeax,Height
  108. jl@yLoop//}
  109. popebx
  110. popedi
  111. popesi
  112. end;
  113. procedureGdipShadow(Data:TBitmapData;Buf:Pointer;Radius:LongWord);
  114. var
  115. Gauss:arrayofInteger;
  116. Q:Double;
  117. x,y,n,z:Integer;
  118. p:PInteger;
  119. begin
  120. //根据半径计算高斯模糊矩阵
  121. Q:=Radius/2;
  122. ifQ=0thenQ:=0.1;
  123. n:=Radiusshl1+1;
  124. SetLength(Gauss,n*n);
  125. p:=@Gauss[0];
  126. z:=0;
  127. forx:=-RadiustoRadiusdo
  128. fory:=-RadiustoRadiusdo
  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. procedureGdipBorder(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:=Expandshl1+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(0,0,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. procedureDrawShadow(constg:TGpGraphics;constBitmap:TGpBitmap;
  164. constlayoutRect: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(0,0,Bitmap.Width,Bitmap.Height),
  173. [imRead,imWrite],pf32bppARGB);
  174. GetMem(Buf,Data.Height*Data.Stride);
  175. try
  176. BackImage(Data,Buf,Color);
  177. ifExpand>ShadowSizethen
  178. Expand:=ShadowSize;
  179. ifExpand<>0then//处理文字阴影扩展
  180. ifExpand<>ShadowSizethen
  181. begin
  182. SaveScan0:=Data.Scan0;
  183. Data.Scan0:=Buf;
  184. GdipBorder(Data,SaveScan0,Expand,Color);
  185. Data.Scan0:=SaveScan0;
  186. endelse
  187. GdipBorder(Data,Buf,Expand,Color);
  188. ifExpand<>ShadowSizethen//处理文字阴影效果
  189. GdipShadow(Data,Buf,ShadowSize-Expand);
  190. finally
  191. FreeMem(Buf);
  192. Bitmap.UnlockBits(Data);
  193. end;
  194. sr:=GpRect(0.0,0.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. procedureDrawShadowString(constg:TGpGraphics;conststr:WideString;
  210. constfont:TGpFont;constlayoutRect:TGpRectF;
  211. ShadowSize,Distance:LongWord;Angle:Single=60;
  212. Color:TARGB=$C0000000;Expand:LongWord=0;
  213. constformat: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)+ShadowSizeshl1+2,
  220. Round(layoutRect.Height+0.5)+ShadowSizeshl1+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[Colorand$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. procedureDrawShadowString(constg:TGpGraphics;conststr:WideString;
  238. constfont:TGpFont;constorigin:TGpPointF;
  239. ShadowSize,Distance:LongWord;Angle:Single=60;
  240. Color:TARGB=$C0000000;Expand:LongWord=0;
  241. constformat: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. procedureTextPaint(g:TGpGraphics);
  2. var
  3. brush:TGpLinearGradientBrush;
  4. font:TGpFont;
  5. fontFamily:TGpFontFamily;
  6. r:TGpRect;
  7. begin
  8. fontFamily:=TGpFontFamily.Create({'TimesNewRoman'}'华文行楷');
  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),5,10,60,$C0000000,1);
  14. DrawShadowString(g,'文字阴影特效',font,GpPoint(10,r.Height/3),1,0,60,$FFFF0000,1);
  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. procedureDrawImageBorder(constg:TGpGraphics;constImage:TGpImage;
  6. x,y:Single;BorderWidth:LongWord;Color:TARGB=kcWhite;
  7. Expand:LongWord=0;constAttributes: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+BorderWidthshl1+2,
  16. Image.Height+BorderWidthshl1+2,
  17. pf32bppARGB);
  18. Bg:=TGpGraphics.Create(Bmp);
  19. Attr:=Attributes;
  20. ifAttr=nilthen
  21. Attr:=TGpImageAttributes.Create;
  22. try
  23. FillChar(ColorMatrix,Sizeof(TColorMatrix),0);
  24. ColorMatrix[3,3]:=1;
  25. ColorMatrix[4,4]:=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. 0,0,layoutRect.Width,layoutRect.Height,utPixel,Attr);
  32. DrawShadow(g,Bmp,layoutRect,BorderWidth,0,0,Color,BorderWidth-Expand);
  33. finally
  34. ifAttributes<>nilthen
  35. Attr.ClearColorMatrix
  36. else
  37. Attr.Free;
  38. Bg.Free;
  39. Bmp.Free;
  40. end;
  41. end;
  42. procedureImagePaint(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,0,0,Image.Width,Image.Height);
  56. //设置图像透明色
  57. Attributes:=TGpImageAttributes.Create;
  58. Attributes.SetColorKey($ff00ff00,$ff00ff00);
  59. //画2个像素的描边图
  60. g.TranslateTransform(Image.Width+20,0);
  61. DrawImageBorder(g,Image,0,0,2,kcWhite,0,Attributes);
  62. g.DrawImage(Image,GpRect(0.0,0,Image.Width,Image.Height),
  63. 0.0,0.0,Image.Width,Image.Height,utPixel,Attributes);
  64. //画5个像素的描边图,其中扩散3像素
  65. g.TranslateTransform(Image.Width+20,0);
  66. DrawImageBorder(g,Image,0,0,5,kcWhite,3,Attributes);
  67. g.DrawImage(Image,GpRect(0.0,0,Image.Width,Image.Height),
  68. 0.0,0.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,可见扩展速度是不大理想的。今天对代码作了一点修改,对每个象素点都进行了判断,如果是边界像素,则作画笔矩阵填充,否则直接跳过,这样一来,速度应该提高不少(没作测试,增加的代码用红色标出,有兴趣者可以测试)。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics