制 作印章来说,主要是如何让字均匀的显示在弧线段上,那么一般的印章要么以圆或者椭圆为底图,不过这两者的算法大致相同,为了方便说明,如下就用相对简单的 圆来举例说明,如果需要做椭圆的话,可以在我的基础上进行扩展,因为核心算法是一样的,相对于圆来说,椭圆求弧长以及各个字符的位置,这两点相对麻烦些, 但是这两者都可找到相应的数学公式。
这里首先提一点,我这篇文章部分借鉴了codeproject的一个例子,原文可以参看如下地址。
http://www.codeproject.com/vb/net/Text_on_Path_with_VBNET.asp
(说实话,这篇文章写得有些乱,而且对于buffer的操作近乎于疯狂)
由于印章的实现相对于这篇文章来说,相对简单多了,而且规律性很强,因此我自己考虑重新组织算法进行实现。
那么实现一个印章,大致步骤如下。
1. 计算字符串总长度,以及各个字符的长度;
2. 计算出字符串的起始角度;
3. 求出每个字符的所在的点,以及相对于中心的角度;
4. 绘制每个字符。
计算字符串总长度,以及各个字符的长度
这里需要用到“Graphics.MeasureString”和“Graphics.MeasureCharacterRanges”这两个方法,由于前者算出来的总长度有问题,所以需要后面进行重新计算(此外,这里我还考虑了字符最后显示方向)。
这部分的代码如下:
/// <summary> /// Compute string total length and every char length /// </summary> /// <param name="sText"></param> /// <param name="g"></param> /// <param name="fCharWidth"></param> /// <param name="fIntervalWidth"></param> /// <returns></returns> private float ComputeStringLength( string sText, Graphics g, float[] fCharWidth, float fIntervalWidth, Char_Direction Direction ) { // Init string format StringFormat sf = new StringFormat(); sf.Trimming = StringTrimming.None; sf.FormatFlags = StringFormatFlags.NoClip | StringFormatFlags.NoWrap | StringFormatFlags.LineLimit; // Measure whole string length SizeF size = g.MeasureString( sText, _font, (int)_font.Style ); RectangleF rect = new RectangleF( 0f,0f, size.Width, size.Height ); // Measure every character size CharacterRange[] crs = new CharacterRange[sText.Length]; for( int i = 0; i < sText.Length; i++ ) crs[i] = new CharacterRange( i, 1 ); // Reset string format sf.FormatFlags = StringFormatFlags.NoClip; sf.SetMeasurableCharacterRanges( crs ); sf.Alignment = StringAlignment.Near; // Get every character size Region[] regs = g.MeasureCharacterRanges( sText, _font, rect, sf ); // Re-compute whole string length with space interval width float fTotalWidth = 0f; for( int i = 0; i < regs.Length; i++ ) { if( Direction == Char_Direction.Center || Direction == Char_Direction.OutSide ) fCharWidth[i] = regs[i].GetBounds( g ).Width; else fCharWidth[i] = regs[i].GetBounds( g ).Height; fTotalWidth += fCharWidth[i] + fIntervalWidth; } fTotalWidth -= fIntervalWidth;//Remove the last interval width return fTotalWidth; }
计算出字符串的起始角度
为了更好地开展文章,那么首先说说在我这篇文章中,坐标的度数位置。详情参看如下图示。
对于图形角度分布有个概念后,那么对于整个字符串所跨的弧度以及起始弧度的计算,就相对比较简单了。具体如下:
// Compute arc\'s start-angle and end-angle double fStartAngle, fSweepAngle; fSweepAngle = fTotalWidth * 360 / ( _rectcircle.Width * Math.PI ); fStartAngle = 270 - fSweepAngle / 2;
求出每个字符的所在的点,以及相对于中心的角度
这一部分相对要麻烦些,大致步骤如下。
1. 通过字符长度,求出字符所跨的弧度;
2. 根据字符所跨的弧度,以及字符起始位置,算出字符的中心位置所对应的角度;
3. 由于相对中心的角度已知,根据三角公式很容易算出字符所在弧上的点,如下图所示;
4. 根据字符长度以及间隔距离,算出下一个字符的起始角度;
5. 重复1直至整个字符串结束。
那么这部分的具体代码如下。
/// <summary> /// Compute every char position /// </summary> /// <param name="CharWidth"></param> /// <param name="recChars"></param> /// <param name="CharAngle"></param> /// <param name="StartAngle"></param> private void ComputeCharPos( float[] CharWidth, PointF[] recChars, double[] CharAngle, double StartAngle ) { double fSweepAngle, fCircleLength; //Compute the circumference fCircleLength = _rectcircle.Width * Math.PI; for( int i = 0; i < CharWidth.Length; i++ ) { //Get char sweep angle fSweepAngle = CharWidth[i] * 360 / fCircleLength; //Set point angle CharAngle[i] = StartAngle + fSweepAngle / 2; //Get char position if( CharAngle[i] < 270f ) recChars[i] = new PointF( _rectcircle.X + _rectcircle.Width / 2 -(float)( _rectcircle.Width / 2 * Math.Sin( Math.Abs( CharAngle[i] - 270 ) * Math.PI / 180 ) ) , _rectcircle.Y + _rectcircle.Width / 2 -(float)( _rectcircle.Width / 2 * Math.Cos( Math.Abs( CharAngle[i] - 270 ) * Math.PI / 180 ) ) ); else recChars[i] = new PointF( _rectcircle.X + _rectcircle.Width / 2 +(float)( _rectcircle.Width / 2 * Math.Sin( Math.Abs( CharAngle[i] - 270 ) * Math.PI / 180 ) ) , _rectcircle.Y + _rectcircle.Width / 2 -(float)( _rectcircle.Width / 2 * Math.Cos( Math.Abs( CharAngle[i] - 270 ) * Math.PI / 180 ) ) ); //Get total sweep angle with interval space fSweepAngle = ( CharWidth[i] + _letterspace ) * 360 / fCircleLength; StartAngle += fSweepAngle; } }
绘制每个字符
由于每个字符所在的点以及相对于圆心的角度都已经计算出来,那么进行绘制就相对简单多了,这里只是通过Matrix来转换一下坐标而已。
具体代码如下。
/// <summary> /// Draw every rotated character /// </summary> /// <param name="g"></param> /// <param name="_text"></param> /// <param name="_angle"></param> /// <param name="_PointCenter"></param> private void DrawRotatedText( Graphics g, string _text, float _angle, PointF _PointCenter ) { // Init format StringFormat sf = new StringFormat(); sf.Alignment = StringAlignment.Center; sf.LineAlignment = StringAlignment.Center; // Create graphics path GraphicsPath gp = new GraphicsPath( System.Drawing.Drawing2D.FillMode.Winding ); int x = (int)_PointCenter.X; int y = (int)_PointCenter.Y; // Add string gp.AddString( _text, _font.FontFamily, (int)_font.Style, _font.Size, new Point( x, y ), sf ); // Rotate string and draw it Matrix m = new Matrix(); m.RotateAt( _angle, new PointF( x,y ) ); g.Transform = m; g.DrawPath( new Pen( _color), gp ); g.FillPath( new SolidBrush( _fillcolor ), gp ); }
以上就是绘制印章的核心算法。对于这个类的调用,如下即可。
TextOnSeal _top = new TextOnSeal(); _top.TextFont = new Font("宋体", 16, FontStyle.Regular); _top.FillColor = Color.Red; _top.ColorTOP = Color.Black; _top.Text = "中华人民共和国"; _top.BaseString = "愚翁专用章"; _top.ShowPath = true; _top.LetterSpace = 20; _top.SealSize = 180; _top.CharDirection = Char_Direction.Center; _top.SetIndent( 20 ); Graphics g = this.CreateGraphics(); g.DrawImage( _top.TextOnPathBitmap(), 0, 0 ); _top.CharDirection = Char_Direction.ClockWise; g.DrawImage( _top.TextOnPathBitmap(), 180, 0 ); _top.CharDirection = Char_Direction.AntiClockWise; g.DrawImage( _top.TextOnPathBitmap(), 0, 180 ); _top.SetIndent( 20 ); _top.CharDirection = Char_Direction.OutSide; g.DrawImage( _top.TextOnPathBitmap(), 180, 180 );
通过如上的代码,可以得到如下的效果。
其实如果做印章来说,还有很多地方需要细化,那么如果网友对此有兴趣,可以在我的基础上进行扩展,在此我就不一一述说。
如下是整个类的完整代码。
//--------------------------- TextOnSeal class --------------------------------------- //------------------------------------------------------------------------------------ //---File: TextOnSeal //---Description: The class file to create seal bitmap with text //---Author: Knight //---Date: Nov.3, 2006 //------------------------------------------------------------------------------------ //---------------------------{TextOnSeal class}--------------------------------------- namespace Seal { using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Diagnostics; /// <summary> /// Summary description for TextOnSeal. /// </summary> public class TextOnSeal { private string _text; private Font _font; private Color _pathcolor = Color.Red; private Color _color = Color.Black; private Color _fillcolor = Color.Black; private int _letterspace = 10; private bool _showpath = true; private Rectangle _rectcircle; private Rectangle _rect; private int _intentlength = 10; private Char_Direction _chardirect = Char_Direction.Center; private int _degree = 90; private string _basestring; #region Class_Properties public Char_Direction CharDirection { get { return _chardirect; } set { if (_chardirect != value) { _chardirect = value; switch (_chardirect) { case Char_Direction.Center: _degree = 90; break; case Char_Direction.ClockWise: _degree = 0; break; case Char_Direction.OutSide: _degree = -90; break; case Char_Direction.AntiClockWise: _degree = 180; break; } } } } public string BaseString { get { return _basestring; } set { _basestring = value; } } public string Text { get { return _text; } set { _text = value; } } public Font TextFont { get { return _font; } set { _font = value; } } public Color PathColor { get { return _pathcolor; } set { _pathcolor = value; } } public Color ColorTOP { get { return _color; } set { _color = value; } } public Color FillColor { get { return _fillcolor; } set { _fillcolor = value; } } public int LetterSpace { get { return _letterspace; } set { _letterspace = value; } } public bool ShowPath { get { return _showpath; } set { _showpath = value; } } public int SealSize { set { _rect = new Rectangle(0, 0, value, value); _rectcircle = new Rectangle( new Point(_rect.X + _intentlength, _rect.Y + _intentlength), new Size(_rect.Width - 2 * _intentlength, _rect.Height - 2 * _intentlength)); } } #endregion {Class_Properties} public void SetIndent(int IntentLength) { _intentlength = IntentLength; _rectcircle = new Rectangle(_intentlength, _intentlength, _rect.Width - _intentlength * 2, _rect.Height - _intentlength * 2); } public TextOnSeal() { // // TODO: Add constructor logic here // } public Bitmap TextOnPathBitmap( Rectangle rectCircle, string strText, Font fntText, Color clrColor, Color clrFill, int nPercentage) { _rect = rectCircle; _rectcircle = new Rectangle( new Point(_rect.X + _intentlength, _rect.Y + _intentlength), new Size(_rect.Width - 2 * _intentlength, _rect.Height - 2 * _intentlength)); _text = strText; _font = fntText; _color = clrColor; _fillcolor = clrFill; _letterspace = nPercentage; return TextOnPathBitmap(); } /// <summary> /// Compute string total length and every char length /// </summary> /// <param name="sText"></param> /// <param name="g"></param> /// <param name="fCharWidth"></param> /// <param name="fIntervalWidth"></param> /// <returns></returns> private float ComputeStringLength(string sText, Graphics g, float[] fCharWidth, float fIntervalWidth, Char_Direction Direction) { // Init string format StringFormat sf = new StringFormat(); sf.Trimming = StringTrimming.None; sf.FormatFlags = StringFormatFlags.NoClip | StringFormatFlags.NoWrap | StringFormatFlags.LineLimit; // Measure whole string length SizeF size = g.MeasureString(sText, _font, (int)_font.Style); RectangleF rect = new RectangleF(0f, 0f, size.Width, size.Height); // Measure every character size CharacterRange[] crs = new CharacterRange[sText.Length]; for (int i = 0; i < sText.Length; i++) crs[i] = new CharacterRange(i, 1); // Reset string format sf.FormatFlags = StringFormatFlags.NoClip; sf.SetMeasurableCharacterRanges(crs); sf.Alignment = StringAlignment.Near; // Get every character size Region[] regs = g.MeasureCharacterRanges(sText, _font, rect, sf); // Re-compute whole string length with space interval width float fTotalWidth = 0f; for (int i = 0; i < regs.Length; i++) { if (Direction == Char_Direction.Center || Direction == Char_Direction.OutSide) fCharWidth[i] = regs[i].GetBounds(g).Width; else fCharWidth[i] = regs[i].GetBounds(g).Height; fTotalWidth += fCharWidth[i] + fIntervalWidth; } fTotalWidth -= fIntervalWidth;//Remove the last interval width return fTotalWidth; } /// <summary> /// Compute every char position /// </summary> /// <param name="CharWidth"></param> /// <param name="recChars"></param> /// <param name="CharAngle"></param> /// <param name="StartAngle"></param> private void ComputeCharPos( float[] CharWidth, PointF[] recChars, double[] CharAngle, double StartAngle) { double fSweepAngle, fCircleLength; //Compute the circumference fCircleLength = _rectcircle.Width * Math.PI; for (int i = 0; i < CharWidth.Length; i++) { //Get char sweep angle fSweepAngle = CharWidth[i] * 360 / fCircleLength; //Set point angle CharAngle[i] = StartAngle + fSweepAngle / 2; //Get char position if (CharAngle[i] < 270f) recChars[i] = new PointF( _rectcircle.X + _rectcircle.Width / 2 - (float)(_rectcircle.Width / 2 * Math.Sin(Math.Abs(CharAngle[i] - 270) * Math.PI / 180)), _rectcircle.Y + _rectcircle.Width / 2 - (float)(_rectcircle.Width / 2 * Math.Cos( Math.Abs(CharAngle[i] - 270) * Math.PI / 180))); else recChars[i] = new PointF( _rectcircle.X + _rectcircle.Width / 2 + (float)(_rectcircle.Width / 2 * Math.Sin(Math.Abs(CharAngle[i] - 270) * Math.PI / 180)), _rectcircle.Y + _rectcircle.Width / 2 - (float)(_rectcircle.Width / 2 * Math.Cos( Math.Abs(CharAngle[i] - 270) * Math.PI / 180))); //Get total sweep angle with interval space fSweepAngle = (CharWidth[i] + _letterspace) * 360 / fCircleLength; StartAngle += fSweepAngle; } } /// <summary> /// Generate seal bitmap /// </summary> /// <returns></returns> public Bitmap TextOnPathBitmap() { // Create bitmap and graphics Bitmap bit = new Bitmap(_rect.Width, _rect.Height); Graphics g = Graphics.FromImage(bit); // Compute string length in graphics float[] fCharWidth = new float[_text.Length]; float fTotalWidth = ComputeStringLength(_text, g, fCharWidth, _letterspace, _chardirect); // Compute arc\'s start-angle and end-angle double fStartAngle, fSweepAngle; fSweepAngle = fTotalWidth * 360 / (_rectcircle.Width * Math.PI); fStartAngle = 270 - fSweepAngle / 2; // Compute every character\'s position and angle PointF[] pntChars = new PointF[_text.Length]; double[] fCharAngle = new double[_text.Length]; ComputeCharPos(fCharWidth, pntChars, fCharAngle, fStartAngle); g.SmoothingMode = SmoothingMode.HighQuality; g.CompositingQuality = CompositingQuality.HighQuality; g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; DrawSealBase(g); // Draw every character for (int i = 0; i < _text.Length; i++) DrawRotatedText(g, _text[i].ToString(), (float)(fCharAngle[i] + _degree), pntChars[i]); g.Dispose(); // Return bitmap return bit; } /// <summary> /// Draw seal base /// </summary> /// <param name="g"></param> private void DrawSealBase(Graphics g) { // Draw background g.FillRectangle(Brushes.Black, _rect); g.FillEllipse(new SolidBrush(_fillcolor), new Rectangle(1, 1, _rect.Width - 2, _rect.Height - 2)); g.FillEllipse(Brushes.White, new Rectangle(4, 4, _rect.Width - 8, _rect.Height - 8)); // Draw start signal StringFormat sf = new StringFormat(); string strStar = "★"; Font fnt = new Font(_font.FontFamily, _font.Size * 3); sf.Alignment = StringAlignment.Center; sf.LineAlignment = StringAlignment.Center; SizeF siz = g.MeasureString(strStar, fnt); g.DrawString(strStar, fnt, new SolidBrush(_fillcolor), new RectangleF(_rect.Width / 2 - siz.Width / 2, _rect.Height / 2 - siz.Height / 2, siz.Width, siz.Height), sf); // Draw base string float[] fCharWidth = new float[_basestring.Length]; float fTotalWidths = ComputeStringLength(_basestring, g, fCharWidth, 0, Char_Direction.Center); float fLeftPos = (_rect.Width - fTotalWidths) / 2; PointF pt; for (int i = 0; i < _basestring.Length; i++) { pt = new PointF(fLeftPos + fCharWidth[i] / 2, _rect.Height / 2 + siz.Height / 2 + 10); DrawRotatedText(g, _basestring[i].ToString(), 0, pt); fLeftPos += fCharWidth[i]; } } /// <summary> /// Draw every rotated character /// </summary> /// <param name="g"></param> /// <param name="_text"></param> /// <param name="_angle"></param> /// <param name="_PointCenter"></param> private void DrawRotatedText(Graphics g, string _text, float _angle, PointF _PointCenter) { // Init format StringFormat sf = new StringFormat(); sf.Alignment = StringAlignment.Center; sf.LineAlignment = StringAlignment.Center; // Create graphics path GraphicsPath gp = new GraphicsPath(System.Drawing.Drawing2D.FillMode.Winding); int x = (int)_PointCenter.X; int y = (int)_PointCenter.Y; // Add string gp.AddString(_text, _font.FontFamily, (int)_font.Style, _font.Size, new Point(x, y), sf); // Rotate string and draw it Matrix m = new Matrix(); m.RotateAt(_angle, new PointF(x, y)); g.Transform = m; g.DrawPath(new Pen(_color), gp); g.FillPath(new SolidBrush(_fillcolor), gp); } } public enum Char_Direction { Center = 0, OutSide = 1, ClockWise = 2, AntiClockWise = 3, } }