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

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;
begin
  oPen := nil;
  oBrush := nil;
  oPath := nil;
  oPathNew := nil;
  oFontFamilyArial := nil;
  oTxtFmt := nil;
  pPnts := nil;
  pPntTypes := nil;
  oMatrix := nil;
  pEqualPnts := nil;
  oFontFamilySong := nil;

  try
    graph.SetSmoothingMode(SmoothingModeAntiAlias);
    graph.SetTextRenderingHint(TextRenderingHintAntiAlias);
    graph.SetInterpolationMode(InterpolationModeHighQualityBicubic);
    graph.SetPageUnit(UnitMillimeter);
    graph.SetPageScale(scale);

    //擦除背景
    if not isSave then
      cBackColor := MakeColor(255, 255, 255)
    else
      cBackColor := MakeColor(0, 255, 255, 255);
    graph.Clear(cBackColor);

    //2. 绘制边框
    if isSave then
    begin
      fOffsetX := 0.0;
      fOffsetX := 0.0;
    end
    else
    begin
      fOffsetX := 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 then
    begin
      fStartAngle := 140.0;
      fEndAngle := 400.0;
    end
    else if iWordCnt > 20 then
    begin
      fStartAngle := 145.0;
      fEndAngle := 390.0;
    end
    else
    begin
      fStartAngle := 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 do
    begin
      wsCur := 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 创建Path
      if Assigned(oPath) then
        FreeAndNil(oPath);
      if Assigned(oPathNew) then
        FreeAndNil(oPathNew);
      if Assigned(oMatrix) then
        FreeAndNil(oMatrix);
      if Assigned(pPnts) then
        FreeMemory(pPnts);
      if Assigned(pPntTypes) then
        FreeMemory(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) then
      FreeAndNil(oPath);
    if Assigned(oPathNew) then
      FreeAndNil(oPathNew);
    if Assigned(oMatrix) then
      FreeAndNil(oMatrix);
    if Assigned(pPnts) then
      FreeMemory(pPnts);
    if Assigned(pPntTypes) then
      FreeMemory(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 = '' then
      Exit;

    //6.1 初始化
    wsCur := Format('(%s)', [stampNo]);
    if Assigned(oPath) then
      FreeAndNil(oPath);
    if Assigned(oPathNew) then
      FreeAndNil(oPathNew);
    if Assigned(oMatrix) then
      FreeAndNil(oMatrix);
    if Assigned(pPnts) then
      FreeMemory(pPnts);
    if Assigned(pPntTypes) then
      FreeMemory(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);
  finally
    if Assigned(oPen) then
      FreeAndNil(oPen);

    if Assigned(oBrush) then
      FreeAndNil(oBrush);

    if Assigned(oPath) then
      FreeAndNil(oPath);

    if Assigned(oPathNew) then
      FreeAndNil(oPathNew);

    if Assigned(oFontFamilyArial) then
      FreeAndNil(oFontFamilyArial);

    if Assigned(oTxtFmt) then
      FreeAndNil(oTxtFmt);

    if Assigned(pPnts) then
      FreeMemory(pPnts);

    if Assigned(pPntTypes) then
      FreeMemory(pPntTypes);

    if Assigned(oMatrix) then
      FreeAndNil(oMatrix);

    if Assigned(pEqualPnts) then
      FreeMemory(pEqualPnts);

    if Assigned(oFontFamilySong) then
      FreeAndNil(oFontFamilySong);
  end;
end;

5. 后记

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

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是使用GDI+绘制异形窗口的示例代码: ```c++ #include <windows.h> #include <gdiplus.h> using namespace Gdiplus; LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { // 初始化GDI+ GdiplusStartupInput gdiplusStartupInput; ULONG_PTR gdiplusToken; GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); // 创建窗口 HWND hwnd; WNDCLASS wc = {}; wc.lpfnWndProc = WindowProc; wc.hInstance = hInstance; wc.lpszClassName = L"MyWindowClass"; RegisterClass(&wc); hwnd = CreateWindowEx(WS_EX_LAYERED | WS_EX_TRANSPARENT, L"MyWindowClass", L"异形窗口", WS_POPUP, 100, 100, 400, 300, NULL, NULL, hInstance, NULL); // 设置窗口透明度 SetLayeredWindowAttributes(hwnd, 0, 128, LWA_ALPHA); // 显示窗口 ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); // 消息循环 MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } // 清理GDI+ GdiplusShutdown(gdiplusToken); return (int)msg.wParam; } LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); // 创建Graphics对象 Graphics graphics(hdc); // 绘制背景 SolidBrush brush(Color(255, 255, 0, 0)); // 红色 graphics.FillRectangle(&brush, 0, 0, 400, 300); // 绘制不规则形状 GraphicsPath path; path.AddEllipse(50, 50, 200, 100); Region region(&path); graphics.SetClip(&region, CombineModeReplace); // 绘制内部内容 SolidBrush innerBrush(Color(255, 0, 255, 0)); // 绿色 graphics.FillRectangle(&innerBrush, 0, 0, 400, 300); EndPaint(hwnd, &ps); } return 0; case WM_ERASEBKGND: return 1; // 留空,避免默认的擦除背景操作 case WM_NCHITTEST: { POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; RECT rc; GetWindowRect(hwnd, &rc); if (PtInRegion(CreateEllipticRgn(50, 50, 250, 150), pt.x - rc.left, pt.y - rc.top)) return HTCLIENT; // 鼠标在不规则形状的区域,返回HTCLIENT表示在客户区 else return HTCAPTION; // 鼠标在其他区域,返回HTCAPTION表示在标题栏 } case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, uMsg, wParam, lParam); } ``` 上述代码示例创建了一个异形窗口,窗口形状为一个椭圆。在WM_PAINT消息处理函数中,使用Graphics对象进行绘制操作,首先绘制了一个红色的背景矩形,然后使用GraphicsPath对象创建了一个椭圆形状的区域,并将其应用到绘图上下文中。最后,绘制了一个绿色的矩形作为内部内容。在WM_NCHITTEST消息处理函数中,根据鼠标坐标和窗口的形状判断鼠标在窗口的哪个区域,并返回相应的结果。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值