自从文章《GDI+ 在Delphi程序的应用 -- 可调节的文字阴影特效》发表后,不少人问我怎样实现文字描边。由于我只是个业余编程爱好者,加上文化底蕴差,只要涉及算法和编程理论方面的东西,我就无能为力了,所以直到目前,我也不知道具体的描边算法是怎样的(网上搜索过N次,也没找到答案,可能这方面的东西是要卖钱的)。
因问得人多了,有时我也思索和研究一下,总算找了个方法可以实现,虽然同专业的图像软件(如PhotoShop)文字描边效果相比差强人意,但可以凑合凑合,作为研究心得,将代码贴在这里备查。
在《GDI+ 在Delphi程序的应用 -- 可调节的文字阴影特效》一文的内容的基础上,对文字阴影效果代码进行了改进和扩充,扩充的功能有2点:一是由原来只能产生黑色阴影扩充为任意颜色阴影;二是可以对阴影进行扩展。有了这2个功能,利用阴影效果也就可以进行文字描边了,推而广之,也可实现图像的描边。下面是具体的代码内容:
- //备份图像。Data:GDI+位图数据,32位ARGB格式;Dest:备份目标;Color:阴影颜色
- procedureBackImage(Data:TBitmapData;Dest:Pointer;Color:TARGB);
- asm
- pushesi
- pushedi
- movesi,[eax+16]//esi=Data.Scan0
- movedi,edx//esi=Dest
- movedx,ecx//edx=Color&0xffffff
- andedx,0FFFFFFh
- movecx,[eax]//ecx=Data.Height*Data.Width
- imulecx,[eax+4]
- cld
- @Loop://for(;ecx>=0;ecx--)
- or[esi],edx
- movsd//*edi++=*esi++&0xff000000|edx
- loop@Loop
- popedi
- popesi
- end;
- //扩展。Data:GDI+位图数据,32位ARGB格式;Source:复制的源
- //ExpMatrix:卷积矩阵;MatrixSize:矩阵大小
- procedureMakeExpand(Data:TBitmapData;Source,ExpMatrix:Pointer;
- MatrixSize:LongWord);
- var
- Radius,mSize,rSize:LongWord;
- x,y:LongWord;
- Width,Height:Integer;
- Matrix:Pointer;
- Stride:LongWord;
- asm
- pushesi
- pushedi
- pushebx
- movesi,edx//esi=Source
- movedi,[eax+16]//edi=Data.Scan0+3(Alphabyte)
- addedi,3
- add ecx, 3
- movMatrix,ecx//Matrix=ExpMatrix +3(Alphabyte)
- movecx,MatrixSize
- movedx,ecx
- dececx
- movebx,[eax]
- subebx,ecx
- movWidth,ebx//Width=Data.Width-(MatrixSize-1)
- movebx,[eax+4]
- subebx,ecx
- movHeight,ebx//Height=Data.Height-(MatrixSize-1)
- shrecx,1
- movRadius,ecx//Radius=MatrixSize/2
- moveax,[eax+8]
- movStride,eax
- movmSize,eax
- shledx,2
- submSize,edx//mSize=Data.Stride-MatrixSize*4
- addeax,4
- imuleax,ecx
- addeax,3
- addesi,eax//esi=esi+(Data.Stride*Radius+Radius*4+3)
- shlecx,3
- movrSize,ecx//rSize=Radius*2*4
- movy,0//for(y=0;y<Height;y++)
- @yLoop://{
- movx,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
- movebx,Stride
- test[esi+ebx],0ffh
- jz@001
- negebx
- test[esi+ebx],0ffh
- jnz@NextPixel
- @001:
- pushedi//Save(edi)
- movebx,Matrix//ebx=Matrix
- movedx,MatrixSize//for(I=0;I<MatrixSize;I++)
- @Loop3://{
- movecx,MatrixSize//for(J=0;J<=MatrixSize;J++)
- @Loop4://{
- mov al,[ebx]//*edi=max(*ebx,*edi)
- cmp al,[edi]
- jb @002
- mov[edi],al
- @002:
- addedi,4//edi+=4
- addebx,4//ebx+=4
- loop@Loop4//}
- addedi,mSize//edi+=mSize
- decedx
- jnz @Loop3//}
- popedi//Reset(edi)
- @NextPixel://}
- addedi,4//edi+=4
- addesi,4//esi+=4
- incx
- moveax,x
- cmpeax,Width
- jl@xLoop//}
- addesi,rSize
- addedi,rSize
- incy
- moveax,y
- cmpeax,Height
- jl@yLoop//}
- popebx
- popedi
- popesi
- end;
- procedureGdipShadow(Data:TBitmapData;Buf:Pointer;Radius:LongWord);
- var
- Gauss:arrayofInteger;
- Q:Double;
- x,y,n,z:Integer;
- p:PInteger;
- begin
- //根据半径计算高斯模糊矩阵
- Q:=Radius/2;
- ifQ=0thenQ:=0.1;
- n:=Radiusshl1+1;
- SetLength(Gauss,n*n);
- p:=@Gauss[0];
- z:=0;
- forx:=-RadiustoRadiusdo
- fory:=-RadiustoRadiusdo
- 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;
- procedureGdipBorder(Data:TBitmapData;Buf:Pointer;Expand:LongWord;Color:TARGB);
- var
- bmp:TGpBitmap;
- bg:TGpGraphics;
- Data1:TBitmapData;
- Size:Integer;
- begin
- Size:=Expandshl1+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;
- procedureDrawShadow(constg:TGpGraphics;constBitmap:TGpBitmap;
- constlayoutRect: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);
- ifExpand>ShadowSizethen
- Expand:=ShadowSize;
- ifExpand<>0then//处理文字阴影扩展
- ifExpand<>ShadowSizethen
- begin
- SaveScan0:=Data.Scan0;
- Data.Scan0:=Buf;
- GdipBorder(Data,SaveScan0,Expand,Color);
- Data.Scan0:=SaveScan0;
- endelse
- GdipBorder(Data,Buf,Expand,Color);
- ifExpand<>ShadowSizethen//处理文字阴影效果
- 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:文字输出格式
- procedureDrawShadowString(constg:TGpGraphics;conststr:WideString;
- constfont:TGpFont;constlayoutRect:TGpRectF;
- ShadowSize,Distance:LongWord;Angle:Single=60;
- Color:TARGB=$C0000000;Expand:LongWord=0;
- constformat:TGpStringFormat=nil);overload;
- var
- Bmp:TGpBitmap;
- Bg:TGpGraphics;
- begin
- //建立透明的32位ARGB阴影位图,大小为layoutRect长、宽度+ShadowSize*2+2
- Bmp:=TGpBitmap.Create(Round(layoutRect.Width+0.5)+ShadowSizeshl1+2,
- Round(layoutRect.Height+0.5)+ShadowSizeshl1+2,
- pf32bppARGB);
- Bg:=TGpGraphics.Create(Bmp);
- try
- Bg.TextRenderingHint:=thAntiAlias;
- //以Color不透明度的黑色画刷,在ShadowSize+1处输出文字到位图画布。
- //方便黑色以外的阴影颜色替换(直接用Color画,模糊处理后很难看)
- Bg.DrawString(str,font,Brushs[Colorand$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替代上面布局矩形外,其他参数同上
- procedureDrawShadowString(constg:TGpGraphics;conststr:WideString;
- constfont:TGpFont;constorigin:TGpPointF;
- ShadowSize,Distance:LongWord;Angle:Single=60;
- Color:TARGB=$C0000000;Expand:LongWord=0;
- constformat:TGpStringFormat=nil);overload;
- begin
- DrawShadowString(g,str,font,g.MeasureString(str,font,origin,format),
- ShadowSize,Distance,Angle,Color,Expand,format);
- end;
上面代码中MakeShadow过程的代码在《GDI+ 在Delphi程序的应用 -- 可调节的文字阴影特效》一文中,本文没有贴出。由于代码中已经有了较详细的注释,故不再解释。下面贴出测试代码:
- procedureTextPaint(g:TGpGraphics);
- var
- brush:TGpLinearGradientBrush;
- font:TGpFont;
- fontFamily:TGpFontFamily;
- r:TGpRect;
- begin
- fontFamily:=TGpFontFamily.Create({'TimesNewRoman'}'华文行楷');
- 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:图像显示属性
- procedureDrawImageBorder(constg:TGpGraphics;constImage:TGpImage;
- x,y:Single;BorderWidth:LongWord;Color:TARGB=kcWhite;
- Expand:LongWord=0;constAttributes:TGpImageAttributes=nil);
- var
- Bmp:TGpBitmap;
- Bg:TGpGraphics;
- ColorMatrix:TColorMatrix;
- Attr:TGpImageAttributes;
- layoutRect:TGpRectF;
- begin
- Bmp:=TGpBitmap.Create(Image.Width+BorderWidthshl1+2,
- Image.Height+BorderWidthshl1+2,
- pf32bppARGB);
- Bg:=TGpGraphics.Create(Bmp);
- Attr:=Attributes;
- ifAttr=nilthen
- 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
- ifAttributes<>nilthen
- Attr.ClearColorMatrix
- else
- Attr.Free;
- Bg.Free;
- Bmp.Free;
- end;
- end;
- procedureImagePaint(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,可见扩展速度是不大理想的。今天对代码作了一点修改,对每个象素点都进行了判断,如果是边界像素,则作画笔矩阵填充,否则直接跳过,这样一来,速度应该提高不少(没作测试,增加的代码用红色标出,有兴趣者可以测试)。