1. 程式人生 > >通過Spire.pdf生成新版稅務局監製章

通過Spire.pdf生成新版稅務局監製章

因為國家稅務局發文,要求將電子發票上舊的稅務局監製章改為新版監製章,按我們當初電子發票的做法,此處部分是將已有的監製章png圖片繪製到指定位置,但讓美工按稅務局行文要求製圖後,生成出來的效果怎麼預覽都不滿意,所以就產生了直接在pdf上繪製稅務局監製章的想法。

想法很美好,現實很骨感!平面幾何知識早就忘到了不知哪個角落,什麼a、b、c,什麼切線,還有最難的計算弧線長度(高等數學),所以事實是知道應該按什麼思路來繪製,但苦於相關數學知識不夠,無從下手。

不過萬能的網際網路上早就有人實現了類似的功能,具體連結在此,第36樓有完整的程式碼。此處雖然是通過Graphics來進行繪製操作,但繪製邏輯部分程式碼很是完整!而最後平移座標系、旋轉角度這些在spire.pdf中都是有對應功能的,所以在此基礎上,加上一些重構調整,雖然有些曲折,但還是產生了如下繪製程式碼(說實在的平面幾何部分完全不懂,所以那部分程式碼只能完整的抄襲了),另外也新增了繪製外部橢圓以及水平方向繪製的功能。

    using Spire.Pdf.Graphics;
    /// <summary>
    /// 印章繪製類
    /// </summary>
    public class StampDrawer
    {
        private float _radiusX, _radiusY;
        private PointF _centerPoint;
        private double _stepAngle;
        private List<Tuple<double, double>> _arcTupleList;
//Tuple.Item1為角度 Tuple.Item2為弦長 /// <summary> /// 建構函式 /// </summary> /// <param name="radiusX">橢圓半長邊長</param> /// <param name="radiusY">橢圓半短邊長</param> /// <param name="centerPoint">中心點</param> /// <param name="stepAngle">以多少角度將橢圓進行分割以進行逼近計算</param>
public StampDrawer(float radiusX, float radiusY, PointF centerPoint, double stepAngle = 0.3d) { if (radiusX <= 0) { throw new ArgumentException(nameof(radiusX)); } if (radiusY <= 0) { throw new ArgumentException(nameof(radiusY)); } if (stepAngle <= 0) { throw new ArgumentException(nameof(stepAngle)); } this._radiusX = radiusX; this._radiusY = radiusY; this._centerPoint = centerPoint; this._stepAngle = stepAngle; this.InitArcTupleList(); } private void InitArcTupleList() { var totalAngle = 360d; var startAngle = -90 - totalAngle / 2; _arcTupleList = new List<Tuple<double, double>>(); _arcTupleList.Add(Tuple.Create(startAngle, 0d)); var angR = startAngle * Math.PI / 180; var lastX = _radiusX * Math.Cos(angR) + _centerPoint.X; var laxtY = _radiusY * Math.Sin(angR) + _centerPoint.Y; for (var i = 1; i <= (int)Math.Ceiling(totalAngle / _stepAngle); i++) { var angle = startAngle + i * this._stepAngle; angR = angle * Math.PI / 180; var x = _radiusX * Math.Cos(angR) + _centerPoint.X; var y = _radiusY * Math.Sin(angR) + _centerPoint.Y; var arcLen = Math.Sqrt((lastX - x) * (lastX - x) + (laxtY - y) * (laxtY - y)); lastX = x; laxtY = y; _arcTupleList.Add(Tuple.Create(angle, _arcTupleList[i - 1].Item2 + arcLen)); } } /// <summary> /// 繪製弧線部分文字內容 /// </summary> /// <param name="canvas">要繪製字型的主體</param> /// <param name="font">繪製要使用的字型</param> /// <param name="brush">繪製要是用的刷子</param> /// <param name="content">繪製內容</param> /// <param name="minRat">從邊線向中心的移動因子</param> /// <param name="fixArc">單字元修正寬度</param> /// <param name="isTop">是否上方向繪製</param> /// <param name="format">PdfStringFormat that represents formatting information, such as line spacing,for the string.</param> public void PaintArcContent(PdfCanvas canvas, PdfFontBase font, PdfBrush brush, string content, float minRat, float fixArc, bool isTop, PdfStringFormat format = null) { var contentSize = font.MeasureString(content); var arcPer = contentSize.Width / content.Length + fixArc; var skipLen = (_arcTupleList[_arcTupleList.Count - 1].Item2 - contentSize.Width - fixArc * (content.Length - 1)) / 2; for (int i = 0; i < content.Length; i++) { var arcL = i * arcPer + arcPer / 2.0 + skipLen; var ang = 0.0d; for (int p = 0; p < _arcTupleList.Count - 1; p++) { if (_arcTupleList[p].Item2 <= arcL && arcL <= _arcTupleList[p + 1].Item2) { ang = (arcL >= ((_arcTupleList[p].Item2 + _arcTupleList[p + 1].Item2) / 2.0)) ? _arcTupleList[p + 1].Item1 : _arcTupleList[p].Item1; if (!isTop) { ang += 180; } break; } } var angR = (ang * Math.PI / 180f); var x = _radiusX * (float)Math.Cos(angR) + _centerPoint.X; var y = _radiusY * (float)Math.Sin(angR) + _centerPoint.Y; var qxang = Math.Atan2(_radiusY * Math.Cos(angR), -_radiusX * Math.Sin(angR)); var fxang = qxang + Math.PI / 2.0; var c = content[isTop ? i : content.Length - 1 - i].ToString(); var charSize = font.MeasureString(c, format); var w = charSize.Width; var h = charSize.Height; if (isTop) { x += h * minRat * (float)Math.Cos(fxang); y += h * minRat * (float)Math.Sin(fxang); x += -w / 2f * (float)Math.Cos(qxang); y += -w / 2f * (float)Math.Sin(qxang); } else { x += (h * minRat + h) * (float)Math.Cos(fxang); y += (h * minRat + h) * (float)Math.Sin(fxang); x += w / 2f * (float)Math.Cos(qxang); y += w / 2f * (float)Math.Sin(qxang); } float rotateAngle = (float)(fxang * 180.0 / Math.PI - 90); if (!isTop) { rotateAngle = rotateAngle + 180; } canvas.TranslateTransform(x, y); canvas.RotateTransform(rotateAngle); canvas.DrawString(c, font, brush, 0, 0, format); canvas.RotateTransform(-rotateAngle); canvas.TranslateTransform(-x, -y); } } /// <summary> /// 繪製水平方向居中顯示的文字 /// </summary> /// <param name="canvas">要繪製字型的主體</param> /// <param name="font">繪製要使用的字型</param> /// <param name="brush">繪製要是用的刷子</param> /// <param name="content">繪製內容</param> /// <param name="relativeToVertical">要繪製的文字中心點相對於橢圓中心點在垂直方向上的偏移量,正值表示向下偏倚,負值表示向上偏倚</param> /// <param name="format">PdfStringFormat that represents formatting information, such as line spacing,for the string.</param> public void PaintHorizontalCenterContent(PdfCanvas canvas, PdfFontBase font, PdfBrush brush, string content, float relativeToVertical = 0, PdfStringFormat format = null) { var fontSize = font.MeasureString(content, format); var x = _centerPoint.X - fontSize.Width / 2; var y = _centerPoint.Y - fontSize.Height / 2 + relativeToVertical; canvas.DrawString(content, font, brush, x, y, format); } /// <summary> /// 按中心點以及半徑填充繪製橢圓 /// </summary> /// <param name="canvas">要繪製的主體</param> /// <param name="brush">繪製要是用的刷子</param> /// <param name="lineWidth">要繪製的橢圓線條寬度</param> /// <param name="radiusPadding">相對於橢圓徑長填充寬度,正值表示向外填充,負值表示向內填充,注意此處不包含橢圓線條寬度</param> public void DrawEllipse(PdfCanvas canvas, PdfBrush brush, float lineWidth, float radiusPadding) { var fx = radiusPadding + lineWidth + _radiusX; var fy = radiusPadding + lineWidth + _radiusY; var rec = new RectangleF(_centerPoint.X - fx, _centerPoint.Y - fy, fx * 2, fy * 2); canvas.DrawEllipse(new PdfPen(brush, lineWidth), rec); } }

原文中區分上下兩部分的角度切割及弧長初始化,在此程式碼中被合併成了只有一次且是按360度進行得初始化,而且調整了原方法中按總角度進行平均分佈的方式,改為按單字寬度加上修正寬度來作為一個字元實際顯示寬度,畢竟按新版監製章要求,下半部分xx稅務局這個長度是會動態變化的。

最後是測試繪製程式碼

            using (var doc = new PdfDocument())
            {
                var page = doc.Pages.Add();
                var stamp1CenterPoint = new Point(50, 50);
                var stamp1RadiusX = 36f;
                var stamp1RaidusY = 22f;
                var draw1 = new StampDrawer(stamp1RadiusX, stamp1RaidusY, stamp1CenterPoint);
                var canvas = page.Canvas;
                var brush = PdfBrushes.Red;
                draw1.DrawEllipse(canvas, brush, 2.8f, 3f);
                draw1.DrawEllipse(canvas, brush, 1, 0.5f);
                var font1 = new PdfTrueTypeFont(new Font("楷體", 7f), true);

                draw1.PaintArcContent(canvas, font1, brush, "全國統一發票監製章", 0, 2, true);
                draw1.PaintHorizontalCenterContent(canvas, font1, brush, "國家稅務總局");
                draw1.PaintArcContent(canvas, font1, brush, "江蘇省稅務局", 0, 0.5f, false);

                var stamp2CenterPoint = new Point(70, 120);
                var font2C = new PdfTrueTypeFont(new Font("仿宋", 10f), true);
                var font2T= new PdfTrueTypeFont(new Font("Arial", 10f, FontStyle.Regular, GraphicsUnit.Point, 174), false);
                var stamp2RadiusX = 48f;
                var stamp2RaidusY = 33f;
                var draw2 = new StampDrawer(stamp2RadiusX, stamp2RaidusY, stamp2CenterPoint);
                draw2.DrawEllipse(canvas, brush, 3.3f, 2f);
                draw2.PaintArcContent(canvas, font2C, brush, "江蘇省某某某有限公司", -0.2f, 6, true);
                draw2.PaintHorizontalCenterContent(canvas, font2C, brush, "某某專用章", 15f);

                doc.SaveToFile("stamp.pdf");
            }

注意此處只是例子,實際生成的監製章與稅務局額要求略有出入,最終效果如下
生成效果