1. 引言

几年前,笔者在做某项目时,需要根据开票方的实际信息(含企业名称,社会信用代码,印章编号)绘制某椭圆形的专用章,加盖到PDF版式文件上,并使用开票方的证书信息进行签名,以防范版式文件伪造、抵赖、冒充和篡改等风险。

至今,依然清晰地记得,对于专用章图片的生成,笔者当时曾经翻阅很多的相关资料,参考过诸多的代码,但所绘制的图片都不太理想。最后,笔者重温解析几何学的知识,经过数日披星戴月,坚持不懈地努力,终于可以绘制出理想的专用章高清PNG图片。

今日,笔者稍得闲暇,愿将笔者摸索的专用章绘制相关的核心算法(该算法使用其它圆形或椭圆形印章),核心的代码进行分享。希望,对遇到类似问题的朋友起到抛砖引玉的作用,同时,由于笔者认知和技术能力有限,难免会有不当或错误之处,欢迎各位朋友不吝赐教。

2. 专用章规范

专用章的规范很简单,笔者摘录如下:

一、形状为椭圆形,尺寸为40×30(mm);

二、边宽1mm;

三、中间为信用代码,18位阿拉伯数字字高3.7mm,字宽1.3mm,18位阿拉伯

数字总宽度26mm(字体为Arial);

四、上方环排中文文字高为4.2mm,环排角度(夹角)210-260度,字与

边线内侧的距离0.5mm(字体为仿宋体);

五、下横排“*​*专用章”文字字高4.6mm,字宽3mm,延章中心线到下

横排字顶端距离4.2mm(字体为仿宋体);

六、专用章下横排号码字高2.2mm,字宽1.7mm,延章中心线到下横排号

码顶端距离10mm(字体为Arial),不需编号时可省去此横排号码。

在常见的公章中以圆形公章和椭圆形公章最为常见。从几何学的角度来说圆是椭圆的一种特殊形式。无论是圆形印章还是椭圆形印章的绘制算法,有两个要点需要掌握。第一个是环绕印章边缘文字的均匀分布的问题,另外一个是环绕印章边缘文字的旋转问题。如果,这两个问题能得心应手,恰到好处的处理,则可以绘制精美的印章。

然而,通过对专用章规范的解读会发现,在规范中对文字的大小的规定,似乎让人有点为难,因为字宽和字高的比例和字体比例不一致,为此必须做特殊的处理才能满足规范。因此专用章除要关注以上两点之外,还要关注对于字体的缩放操作。

下面,笔者对以上三点所涉及的核心算法做简要分析。

3. 专用章绘制核心算法分析

3.1 环绕印章边缘文字的均匀分布

在圆形印章和椭圆形印章中,环绕印章边缘的文字都是以圆心(对于椭圆来说是中心)为中心,环绕印章边缘均与分布,这样从视觉上来看才是舒服和完美的。所谓的均匀分布,是指相邻的文字之间的弧长相等。对于圆形印章来说做到这一点相对来说比较容易,因为只要控制相邻的文字之间的圆心角相等即可。但对于椭圆形印章来说,圆心角相等则对应的弧长未必相等,所以椭圆形印章环绕印章边缘文字的均与分布,相对来说计算比较复杂。

熟悉椭圆性质的朋友都知道,椭圆的周长和弧长没有精确的计算公式,只能借鉴积分进行近似的计算。如图-1所示,使用平面解析几何学知识,可以根据椭圆的圆心角θ,计算出对应的舷长ℓ。如果圆心角θ足够小,我们则可以认为圆心角θ对应的弧长等于弦长ℓ。关于椭圆的弦长计算公式的推导,笔者不再展开赘述,有兴趣的朋友可以查阅相关资料,或者和笔者做进一步的沟通交流。使用上述理论,我可以将椭圆的弧分割成若干段,然后对求出每一段弧所对应的弦长,延后求和,便可近似的得到椭圆的弧长。我们分割弧度随对应的圆心角θ越小,则计算结果越精确。

图-1 椭圆弧长的计算

环绕椭圆印章边缘均匀分布文字,首先需要计算出起始弦和结束弦之间的弧长,然后进行等分。行之有效的方法是,使θ足够小,分割弧,计算出对应的弦长,在内存中建立θ和弦长对应的二维表,然后再计算出弧上的等分点即可。如图-2所示,笔者使用上述理论,将X轴上方的椭圆弧,进行十等分的效果,左边使用θ=1度进行计算,右边使用θ=0.0025度进行计算所的效果。通过对比,可以证明如果θ越小,等分效果越精确。

图-2θ越小计算椭圆弧长越精确效果对比

3.2 环绕印章边缘文字的旋转

在计算机中常规的字体打印,字体是垂直于字体基线的。在圆形印章和椭圆形印章中,环绕印章边缘文字的基线,需要平行于文字输出点的切线,这样才显得整洁和美观,如图-3所示。为了满座该原则,在进行环绕印章边缘文字的旋转操作时,首先,需要计算出文字输出点切线的角度。然后,旋转坐标系平行椭圆切线。最后,文字输出点根据坐标系旋转角度进行坐标位置变换,输出文本。

图-3 旋转文字基线平行于输出点切线效果图

3.3 通过仿射变换对字体大小进行缩放

由于专用章规范中所规定的字高和字宽的比例,和字体本身的比例不一致,如果通过绘制文本(drawstring)的方式来实现,显然是不行的。有经验的朋友会自然而然地会想到,是否可以通过仿射变换来进行处理呢?

所谓仿射变换就是“线性变换”加“平移”,其满足以下三点,1.变换前是直线,变换后依然是直线;2. 直线比例保持不变;3.变换前是原点,变换后依然是原点。仿射变换只需构造两行三列的仿射变换矩阵即可实现图形的缩放(scale),旋转(rotate),切变(share),平移(translate)。显然,仿射变换是可以达到我们的目标的。有关仿射变换更多的细节,笔者不再展开叙述,有兴趣的朋友可查阅相关的资料,或者和笔者做进一步的沟通交流。

然而,仿射变换是对图像进行处理的,不可以对文字直接处理。如果要对文字进行处理,需要首先对文字进行栅格化,然后再做仿射变换。

为了满足专用章对字高和字宽的限定,我们需要对文本进行栅格化处理,再构造构造用于缩放(scale)计算的仿射矩阵,最后对栅格化后的文本进行仿射变换即可。需要特别注意的是,进行仿射变换后,图像(栅格化后的文本)会产生位移,需要应用特殊的处理以解决问题。

如果对以上三点的核心算法能够掌握,想必绘制符合规范要求的专用章应该不在话下。下面将笔者开发的专用章绘制工具及核心代码分享给各位朋友。

4. 使用GDI+绘制专用章核心代码分享

下面将设计算法的核心代码进行分享,是该代码绘制的椭圆形印章的效果如下图所示。

图-4 使用GDI+绘制的椭圆形印章效果图

{************************************************************************
函数:drawStamp
功能:绘制专用章
参数:graph           输入参数,GDI+ Graphics对象corpName        输入参数,公司名称nsrsbh          输入参数,信用代码stampNo         输入参数,印章编号scale           输入参数,缩放比例isSave          输入参数,是否保存图片文件
作者:海之边  QQ-3094353627************************************************************************}
procedure drawStamp(graph: TGPGraphics; corpName: WideString;nsrsbh: WideString; stampNo: WideString; scale: Single; isSave: Boolean);
var iPntCnt, iWordCnt, idx: Integer;fOffsetX, fOffSetY, fWid, fHigh, fAInter, fBInter, fStartAngle, fEndAngle: Single;fTagent: Single;cBackColor: Cardinal;wsCur: WideString;sTag: string;pPnts, pEqualPnts, pPntCur: PGPPointF;pPntTypes: PByte;rPntStart, rPntCenter, rPntOrigin, rCurPnt: TGPPointF;oPen: TGPPen;oBrush: TGPSolidBrush;oPath, oPathNew: TGPGraphicsPath;oFontFamilyArial, oFontFamilySong: TGPFontFamily;oTxtFmt: TGPStringFormat;oMatrix: TGPMatrix;s: TStatus;
beginoPen := nil;oBrush := nil;oPath := nil;oPathNew := nil;oFontFamilyArial := nil;oTxtFmt := nil;pPnts := nil;pPntTypes := nil;oMatrix := nil;pEqualPnts := nil;oFontFamilySong := nil;trygraph.SetSmoothingMode(SmoothingModeAntiAlias);graph.SetTextRenderingHint(TextRenderingHintAntiAlias);graph.SetInterpolationMode(InterpolationModeHighQualityBicubic);graph.SetPageUnit(UnitMillimeter);graph.SetPageScale(scale);//擦除背景if not isSave thencBackColor := MakeColor(255, 255, 255)elsecBackColor := MakeColor(0, 255, 255, 255);graph.Clear(cBackColor);//2. 绘制边框if isSave thenbeginfOffsetX := 0.0;fOffsetX := 0.0;endelsebeginfOffsetX := 5.0;fOffSetY := 5.0;end;fWid := 40.0;fHigh := 30.0;oPen := TGPPen.Create(MakeColor(255, 0, 0), 1);rPntStart.X := fOffsetX + 0.5;rPntStart.Y := fOffSetY + 0.5;graph.DrawEllipse(oPen, rPntStart.X, rPntStart.Y, fWid - 1, fHigh - 1);//2. 平移坐标原点到椭圆中心rPntCenter.X := fOffsetX + fWid / 2;rPntCenter.Y := fOffSetY + fHigh / 2;graph.TranslateTransform(rPntCenter.X, rPntCenter.Y);//3. 绘制税号(社会信用代码)//3.1. 初始化oBrush := TGPSolidBrush.Create(MakeColor(255, 0, 0));rPntOrigin.X := 0;rPntOrigin.Y := 0;oFontFamilyArial := TGPFontFamily.Create('Arial');oTxtFmt := TGPStringFormat.Create;oTxtFmt.SetAlignment(StringAlignmentCenter);oTxtFmt.SetLineAlignment(StringAlignmentCenter);//3.2 创建文本Path并栅格化oPath := TGPGraphicsPath.Create;oPath.AddString(nsrsbh, Length(nsrsbh), oFontFamilyArial, FontStyleRegular,2.273, rPntOrigin, oTxtFmt);iPntCnt := oPath.GetPointCount;pPnts := GetMemory(iPntCnt * SizeOf(TGpPointF));oPath.GetPathPoints(pPnts, iPntCnt);pPntTypes := GetMemory(iPntCnt);oPath.GetPathTypes(pPntTypes, iPntCnt);//3.3 进行缩放仿射变换scalAffine(1, 1.3121, pPnts, iPntCnt);oPathNew := TGPGraphicsPath.Create(pPnts, pPntTypes, iPntCnt);//3.4 绘制税号graph.FillPath(oBrush, oPathNew);//4. 绘制公司名称//4.1 设置公司名称外对齐轮廓椭圆的长轴和短轴fAInter := 16.4;fBInter := 11.4;//4.2 设置公司名称在椭圆上的绘制范围iWordCnt := Length(corpName);if iWordCnt >= 23 thenbeginfStartAngle := 140.0;fEndAngle := 400.0;endelse if iWordCnt > 20 thenbeginfStartAngle := 145.0;fEndAngle := 390.0;endelsebeginfStartAngle := 165.0;fEndAngle := 375.0;end;oFontFamilySong := TGPFontFamily.Create('仿宋');//4.3 平分弧pEqualPnts := self.splitEllipseArc(fAInter, fBInter, fStartAngle,fEndAngle, iWordCnt);//4.5 逐字绘制pPntCur := pEqualPnts;for idx := 1 to iWordCnt dobeginwsCur := corpName[idx];//4.5.1 计算切线角度fTagent := calcEllipseTangentLineDegree(fAInter, fBInter, pPntCur^.X,pPntCur^.Y);//4.5.2 平移坐标系原点到等点graph.TranslateTransform(pPntCur^.X, pPntCur^.Y);//4.5.3 创建Pathif Assigned(oPath) thenFreeAndNil(oPath);if Assigned(oPathNew) thenFreeAndNil(oPathNew);if Assigned(oMatrix) thenFreeAndNil(oMatrix);if Assigned(pPnts) thenFreeMemory(pPnts);if Assigned(pPntTypes) thenFreeMemory(pPntTypes);oPath := TGPGraphicsPath.Create;rPntOrigin.X := 0.0;rPntOrigin.Y := 0.0;oPath.AddString(wsCur, Length(wsCur), oFontFamilySong, FontStyleRegular,3.3158, rPntOrigin, oTxtFmt);iPntCnt := oPath.GetPointCount;pPnts := GetMemory(iPntCnt * SizeOf(TGpPointF));oPath.GetPathPoints(pPnts, iPntCnt);pPntTypes := GetMemory(iPntCnt);oPath.GetPathTypes(pPntTypes, iPntCnt);//4.5.4 旋转仿射变换rotateAffine(fTagent * 180.0 / Pi, pPnts, iPntCnt);oPathNew := TGPGraphicsPath.Create(pPnts, pPntTypes, iPntCnt);graph.FillPath(oBrush, oPathNew);//4.5.5 恢复坐标系graph.TranslateTransform(pPntCur^.X * -1, pPntCur^.Y * -1);Inc(pPntCur);end;//5. 绘制"XX专用章"//5.1 初始化wsCur := 'XX专用章';if Assigned(oPath) thenFreeAndNil(oPath);if Assigned(oPathNew) thenFreeAndNil(oPathNew);if Assigned(oMatrix) thenFreeAndNil(oMatrix);if Assigned(pPnts) thenFreeMemory(pPnts);if Assigned(pPntTypes) thenFreeMemory(pPntTypes);//5.2 平移坐标系原点graph.TranslateTransform(0, 7.5);//5.3 输出文本并栅格化oPath := TGPGraphicsPath.Create;rPntOrigin.X := 0;rPntOrigin.Y := 0;oPath.AddString(wsCur, Length(wsCur), oFontFamilySong, FontStyleRegular,2.201, rPntOrigin, oTxtFmt);iPntCnt := oPath.GetPointCount;pPnts := GetMemory(iPntCnt * SizeOf(TGpPointF));oPath.GetPathPoints(pPnts, iPntCnt);pPntTypes := GetMemory(iPntCnt);oPath.GetPathTypes(pPntTypes, iPntCnt);//5.4 仿射变换scalAffine(1, 1.6487, pPnts, iPntCnt);oPathNew := TGPGraphicsPath.Create(pPnts, pPntTypes, iPntCnt);//5.5 输出graph.FillPath(oBrush, oPathNew);//5.6 还原坐标系原点graph.TranslateTransform(0, -7.5);//6. 绘制印章编号if stampNo = '' thenExit;//6.1 初始化wsCur := Format('(%s)', [stampNo]);if Assigned(oPath) thenFreeAndNil(oPath);if Assigned(oPathNew) thenFreeAndNil(oPathNew);if Assigned(oMatrix) thenFreeAndNil(oMatrix);if Assigned(pPnts) thenFreeMemory(pPnts);if Assigned(pPntTypes) thenFreeMemory(pPntTypes);//6.2 平移坐标系原点graph.TranslateTransform(0, 11.1);//6.3 输出文本并栅格化oPath := TGPGraphicsPath.Create;rPntOrigin.X := 0;rPntOrigin.Y := 0;oPath.AddString(wsCur, Length(wsCur), oFontFamilyArial, FontStyleRegular,1.8730, rPntOrigin, oTxtFmt);iPntCnt := oPath.GetPointCount;pPnts := GetMemory(iPntCnt * SizeOf(TGpPointF));oPath.GetPathPoints(pPnts, iPntCnt);pPntTypes := GetMemory(iPntCnt);oPath.GetPathTypes(pPntTypes, iPntCnt);//6.4 仿射变换scalAffine(1, 1.9442, pPnts, iPntCnt);oPathNew := TGPGraphicsPath.Create(pPnts, pPntTypes, iPntCnt);//6.5 输出graph.FillPath(oBrush, oPathNew);//6.6 还原坐标系graph.TranslateTransform(0, -11.1);finallyif Assigned(oPen) thenFreeAndNil(oPen);if Assigned(oBrush) thenFreeAndNil(oBrush);if Assigned(oPath) thenFreeAndNil(oPath);if Assigned(oPathNew) thenFreeAndNil(oPathNew);if Assigned(oFontFamilyArial) thenFreeAndNil(oFontFamilyArial);if Assigned(oTxtFmt) thenFreeAndNil(oTxtFmt);if Assigned(pPnts) thenFreeMemory(pPnts);if Assigned(pPntTypes) thenFreeMemory(pPntTypes);if Assigned(oMatrix) thenFreeAndNil(oMatrix);if Assigned(pEqualPnts) thenFreeMemory(pEqualPnts);if Assigned(oFontFamilySong) thenFreeAndNil(oFontFamilySong);end;
end;

5. 后记

笔者期望能通过这篇文章,对有相同业务背景,或遇到相关技术障碍的朋友,能起到抛砖引玉的作用。由于篇幅有限,诸多的细节不能一一展开叙述,如有需要,笔者也坦诚地期盼能和各位朋友做进一步的沟通交流,相互学习,共同受益。

椭圆形印章核心算法浅析及使用GDI+绘制椭圆印章的方法相关推荐

  1. python画椭圆形函数算法_python如何使用matplotlib绘制椭圆的数学公式-百度经验

    python是一门非常实用的编程语言,matplotlib库是python的可视化的实现方式,matplotlib提供了对latex的实现,那怎样用matplotlib绘制椭圆的公式呢? 工具/原料 ...

  2. HTML5印章绘制电子签章图片,中文英文椭圆章、中文英文椭圆印章 电子签章图片采集

    电子签章图片采集 印章图片的采集两种互补方式: 方式1:在线生成印章图片方式,但是这种方式有个弊端,对印章中公司名称字数有限制,字数越多可能就完蛋了. 方式2:上传印章扫描件,系统来对扫描图片进行处理 ...

  3. HTML5印章绘制电子签章图片,中文英文椭圆章、中文英文椭圆印章

    HTML5印章绘制电子签章图片,中文英文椭圆章.中文英文椭圆印章 原文:HTML5印章绘制电子签章图片,中文英文椭圆章.中文英文椭圆印章 电子签章图片采集 印章图片的采集两种互补方式: 方式1:在线生 ...

  4. 卡尔曼滤波器求速度matlab,卡尔曼滤波器算法浅析及matlab实战

    原标题:卡尔曼滤波器算法浅析及matlab实战 作者:Liu_LongPo 出处:Liu_LongPo的博客 卡尔曼滤波器是一种利用线性系统状态方程,通过系统输入输出观测数据,对系统状态进行最优估计的 ...

  5. Paxos 算法浅析

    Paxos 算法浅析  xiewen 关注 2016.07.19 17:24* 字数 3613 阅读 3740评论 0喜欢 22 持续更新 如何浅显易懂地解说 Paxos 的算法? 参考资料 #8:知 ...

  6. 阿里资深AI工程师教你逐个击破机器学习核心算法

    01 近年来,随着 Google 的 AlphaGo 打败韩国围棋棋手李世乭之后,机器学习尤其是深度学习的热潮席卷了整个 IT 界. 所有的互联网公司,尤其是 Google 微软,百度,腾讯等巨头,无 ...

  7. 木桶排序算法_【生信常识】二代测序的比对算法浅析

    前言 本来我只打算将孟大哥的视频内容做一个文字版的概述,然后孟大哥说,不如再加一个算法推导吧,然后我就开始看多一些东西,然后就想着把孟大哥视频里面大概提及然后没有仔细讲的部分做一些补充,完善整个体系的 ...

  8. 核心算法缺位,人工智能发展面临“卡脖子”窘境

    http://www.xinhuanet.com/politics/2019-04/30/c_1124435131.htm "徐匡迪之问"引发业界共鸣-- 核心算法缺位,人工智能发 ...

  9. SQL关键字转换大写核心算法实现

    1 不跟你多废话 上代码! /// <summary>/// SQL关键字转换器/// </summary>public class SqlConverter : IKeywo ...

最新文章

  1. 漫谈 C++ 的各种检查
  2. Java线程池理解及用法
  3. 作为一个程序员,进步完全取决于自己
  4. Unknown encoder ‘libx264‘的解决方法
  5. php怎么引入外部css文件,js如何引入css外部文件
  6. Number 和 Math 类
  7. foreach循环符合就不往下走了_Java基础入门篇——For循环
  8. sqlsrv 过去一小时_包裹分拣神器!每小时8.6万件包裹,是整个车间人数的两倍...
  9. 购买域名以及申请证书
  10. Oracle监控的关键指标有哪些
  11. Navicat注册机报错No all pattern found! file already patched
  12. 统计|如何理解两个总体均值之差的区间估计的计算
  13. JNCIS-FWV Study Guide v1.3
  14. JavaWeb 之 Listener监听器及Session的钝化与活化
  15. Wireshark入门-Wireshark
  16. 如何解决EXCEL中弹出“信息检索”的信息
  17. webgl1到webgl2_我如何使用WebGL重建Gorillaz Andromeda音乐视频
  18. 各品牌电脑进入BIOS的按键
  19. 首经贸电子信息复试软件工程导论
  20. 郭德纲的网络效应和网络利用

热门文章

  1. (一)制作U盘启动盘
  2. 小红书推广方式和技巧有哪些?
  3. Deployer 部署项目
  4. Harris角点检测,及其Matlab和OpenCV实现
  5. 【向题看齐】408之计算机组成原理概念记忆总结
  6. 一个请求式分页存储管理系统中计算机应用,操作系统概论自考2012年7月真题
  7. 旋转正方体加径向渐变
  8. Go Web快速开发框架 Fiber
  9. 对策论基础---矩阵对策的基本定理
  10. 蓝桥杯 ALGO-131 Beaver's Calculator