通過Spire.pdf生成新版稅務局監製章
阿新 • • 發佈:2018-12-12
因為國家稅務局發文,要求將電子發票上舊的稅務局監製章改為新版監製章,按我們當初電子發票的做法,此處部分是將已有的監製章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");
}
注意此處只是例子,實際生成的監製章與稅務局額要求略有出入,最終效果如下