本文以发票助手获取发票信息为例,详细介绍如何使用 .NET 技术处理 PDF 文件并进行二维码解析。文章介绍的相关代码已开源在GitHub,欢迎查看和收藏。
1. 背景
在日常工作中,我们经常需要处理发票信息,比如日常报销和出差等等场景。如果发票比较多,能有一款工具可以方便的帮助我们处理 PDF 发票就非常棒了,当然我们其实有很多的选则,比如微信的卡包,还有QQ邮箱、WPS的发票工具等等,但是这些工具都有一些局限性。因此,我们可以自己开发一款发票助手,通过读取 PDF 文件中的发票二维码并解析其内容,实现发票信息的提取和处理。
2. 发票的变化
电子发票越来越普及,我们可以通过扫码获取电子发票,也可以通过邮件或者网站下载电子发票。电子发票的好处是方便快捷,不需要纸质发票,可以随时随地查看和打印。
不知道大家有没有发现,最近的发票都已经换成了新的数电发票,增值税专用发票也使用了电子化,省去了邮寄的环节,减少了开票时间和开票成本。数电发票增加XML的数据电文格式便利交付,更加方便了信息的提取,同时也保留PDF/OFD格式。
3. 发票中的二维码
无论是之前的电子发票还是新的数电发票,在发票的左上角一般都会有一个二维码,里面通常包含了发票的关键信息,如发票代码、发票号码、金额、日期等。我们可以通过解析二维码来获取这些信息,从而实现发票信息的提取和处理。当然,这些信息我们也可以直接通过微信扫码来获取和测试。
以下是一个发票二维码的扫码获取的信息的示例,部分数字信息已经做了模糊处理,使用x代替了数字:
01,10,011002400xxx,35602xxx,1058.34,20240xxx,016258xxx15879380xxx,Fxxx,01,31,,24117000000xxx133xxx,476.60,20241xxx,,7xxx01,32,,24117000000xxx771xxx,1472.24,20241xxx,,6xxx
上面的第一个发票是今年早些时候的老款电子发票,第二个和第三个是新款数电发票。下面,我们重点分析一下属性对应的内容及含义,他们的信息以逗号为分隔符:
这里第一个固定属性值 01,第二个属性值 10 则是发票类型,具体含义如下:
后面几个分别是发票的代码、号码、金额、日期和校验码等信息。
4. 使用 .NET 处理 PDF 文件和二维码解析
了解了发票中二维码的信息后,我们可以使用 .NET 技术来处理 PDF 文件并进行二维码解析。在这里,我们将使用 UglyToad.PdfPig 库来读取 PDF 文件,使用 ZXing 库来解析二维码。
4.1. 准备工作
在开始之前,请确保你已经安装了以下 NuGet 包:
oUglyToad.PdfPigoZXing.Net
你可以通过以下命令安装这些包:
dotnet add package PdfPigdotnet add package ZXing.Net
4.2. 获取PDF文件中的二维码图片并解析
首先,我们需要读取 PDF 文件中的第一个页面,并获取其中的第一个图像。然后,我们将该图像转换为 Bitmap 对象,并使用 ZXing 库的 BarcodeReader 对象解析二维码。最后,我们将解析出的发票信息添加到 DataGridView 控件中。
using (PdfDocument document = PdfDocument.Open(fullname)){Page firstPage = document.GetPages().FirstOrDefault();if (firstPage != ){var firstImage = firstPage.GetImages().FirstOrDefault();if (firstImage != ){var bitmap = ConvertPdfImageToBitmap(firstImage);var result = reader.Decode(bitmap);if (result != ){string[] values = result.Text.Split(',');if (values.Length < 8) break;dgvPdfFiles.Rows.Add(file.FullName, file.Name, values[3], values[5], values[4]);}}}}
当然,实际情况可能更加复杂,你可能需要多个图像的问题,并不一定所有的PDF文件第一个图片就是二维码,你可能需要根据具体的情况来处理。比如可以通过判断图片的大小来确定是否是二维码。
var images = firstPage.GetImages().Where(i => i.HeightInSamples == i.WidthInSamples && i.WidthInSamples > 100 && i.HeightInSamples > 100);var firstImage = images.FirstOrDefault();
4.3. 发票其他信息提取
除了二维码中的信息,我们还可以通过读取 PDF 文件的文本内容来提取发票的其他信息,比如项目明细或是在非数电发票的情况下,我们需要通过文本内容来提取发票信息的含税金额信息。因为数电发票是含税的,之前的发票是不含税的,所以我们需要根据具体的情况来处理。这里我们可以使用 UglyToad.PdfPig 库的 Page.Text 属性来获取页面的文本内容。
以下是相关的正则表达式,用于匹配发票的日期、号码、类目和金额等信息:
/// <summary>/// 正则匹配年月日/// 开票日期[::]\s*\d{4}年\d{2}月\d{2}日/// </summary>private static readonly Regex dateRegex = new Regex(@"\d{4}年\d{2}月\d{2}日", RegexOptions.Compiled);/// <summary>/// 匹配发票号码/// </summary>private static readonly Regex noRegex = new Regex(@"发票号码[::]\s*(\d+)", RegexOptions.Compiled);/// <summary>/// 匹配类目/// 匹配到第一个,然后去除两边的*号/// </summary>private static readonly Regex typeRegex = new Regex(@"\*.*?\*", RegexOptions.Compiled);/// <summary>/// 匹配金额/// </summary>private static readonly Regex amountRegex2 = new Regex(@"[yen¥]\s*([0-9]+[.][0-9]{2})", RegexOptions.Compiled );
这里简单介绍一下类目和发票号码的匹配,其他的匹配可以根据具体的情况来处理:
// 处理类目var typeMatch = typeRegex.Match(text);if (typeMatch.Success){var type = typeMatch.Value.Trim('*');dgvPdfFiles.Rows[dgvPdfFiles.Rows.Count - 1].Cells["InvoiceType"].Value = type;}// 处理发票号码if(dgvPdfFiles.Rows[dgvPdfFiles.Rows.Count - 1].Cells["InvoiceNo"].Value.ToString() == "?"){var noMatch = noRegex.Match(text);if (noMatch.Success){var no = noMatch.Groups[1].Value;dgvPdfFiles.Rows[dgvPdfFiles.Rows.Count - 1].Cells["InvoiceNo"].Value = no;}}
当然,实际的情况可能更加复杂,比如之前的发票可能存在密码区,会造成文本内容的提取不准确等。不过,后面的发票都是数电发票,不存在这个问题了。而且出了XML的数据电文格式,更加方便了信息的提取,没必要这么麻烦了。
4.4. 发票信息表
将提取的发票信息添加到 DataGridView 控件中,除了方便我们查看和管理外。这里我们也可以通过 DataGridView 导出 Excel 表格,以下代码展示了如何将发票信息导出为 CSV 文件:
/// <summary>/// 导出/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private void btnExport_Click(object sender, EventArgs e){// 将列表导出CSV文件using (SaveFileDialog sfd = new SaveFileDialog(){FileName = "发票数据.csv",Filter = "CSV文件|*.csv",Title = "保存CSV文件"}){if (sfd.ShowDialog() == DialogResult.OK){string outputFilePath = sfd.FileName;using (StreamWriter sw = new StreamWriter(outputFilePath, false, Encoding.UTF8)){// 带 BOM 的 UTF-8 文件头sw.WriteLine("\uFEFF文件名,发票号码,开票日期,开票类目,金额");foreach (DataGridViewRow row in dgvPdfFiles.Rows){string fileName = row.Cells["FileName"].Value.ToString();string invoiceNo = row.Cells["InvoiceNo"].Value.ToString();string invoiceDate = row.Cells["InvoiceDate"].Value.ToString();string invoiceType = row.Cells["InvoiceType"].Value.ToString();string invoiceAmount = row.Cells["InvoiceAmount"].Value.ToString();sw.WriteLine($"{fileName},{invoiceNo},{invoiceDate},{invoiceType},{invoiceAmount}");}}// 询问是否打开文件if (MessageBox.Show("CSV文件导出完成,是否打开?", "提示", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes){System.Diagnostics.Process.Start(outputFilePath);}txtStatus.Text = "CSV文件导出完成";}}}
在我们提取了类目之后,我们也可以通过类目来统计发票的总额,这样可以方便我们进行发票管理和统计。
/// <summary>/// 导出发票类目及金额信息/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private void btnExportType_Click(object sender, EventArgs e){// 将列表导出CSV文件using (SaveFileDialog sfd = new SaveFileDialog(){FileName = "发票类目金额.csv",Filter = "CSV文件|*.csv",Title = "保存CSV文件"}){if (sfd.ShowDialog() == DialogResult.OK){string outputFilePath = sfd.FileName;using (StreamWriter sw = new StreamWriter(outputFilePath, false, Encoding.UTF8)){// 带 BOM 的 UTF-8 文件头sw.WriteLine("\uFEFF开票类目,金额");var query = dgvPdfFiles.Rows.Cast<DataGridViewRow>().GroupBy(r => r.Cells["InvoiceType"].Value.ToString()).Select(g => new{InvoiceType = g.Key,Amount = g.Sum(r => Convert.ToDecimal(r.Cells["InvoiceAmount"].Value))});foreach (var item in query){sw.WriteLine($"{item.InvoiceType},{item.Amount}");}}// 询问是否打开文件if (MessageBox.Show("CSV文件导出完成,是否打开?", "提示", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes){System.Diagnostics.Process.Start(outputFilePath);}txtStatus.Text = "CSV文件导出完成";}}}
5. PDF合并和打印
除了提取发票信息,我们还可以使用 .NET 技术来实现 PDF 文件的合并和打印。比如,我们可以将多个发票 PDF 文件合并成一个 PDF 文件,或者直接打印发票 PDF 文件。这样可以方便我们进行发票管理和归档。
将PDF文件合并成一个PDF文件可以方便我们进行打印,这样在打印的时候可以方便调整每张纸打印的页数,比如可以打印两张或者四张等等。
// 合并PDF文件private void MergePdfFiles(string[] pdfFiles, string outputFilePath){PdfDocumentBuilder builder = new PdfDocumentBuilder();foreach (string pdfFile in pdfFiles){using (PdfDocument inputDocument = PdfDocument.Open(pdfFile)){for (var i = 0; i < inputDocument.NumberOfPages; i++){builder.AddPage(inputDocument, i + 1);}}}//保存PDF文件var documentBytes = builder.Build();File.WriteAllBytes(outputFilePath, documentBytes);}
其实打印PDF文件也很简单,当然这个只是最简单的实现方式,调用系统打开PDF文件,然后发送打印指令,这样就可以打印PDF文件了。
/// <summary>/// 打印指定文件/// </summary>/// <param name="tempPdfFile"></param>private async void PrintPdfFile(string tempPdfFile){System.Diagnostics.Process.Start("explorer", tempPdfFile);await Task.Delay(1000);// 发送 Ctrl + PSendKeys.SendWait("^(p)");}
6. 总结
通过以上代码,我们展示了如何使用 .NET 结合 UglyToad.PdfPig 和 ZXing 库从 PDF 文件获取图片,并解析二维码信息,同时介绍了如何提取发票的其他信息,如日期、号码、类目和金额等。最后,我们还展示了如何将提取的发票信息导出为 CSV 文件,以及如何合并和打印 PDF 文件。希望这篇文章能帮助你更好地理解和实现发票信息的提取和处理。如果你有任何问题或建议,欢迎在评论区留言