本文以发票助手获取发票信息为例,详细介绍如何使用 .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,,7xxx
01,32,,24117000000xxx771xxx,1472.24,20241xxx,,6xxx
上面的第一个发票是今年早些时候的老款电子发票,第二个和第三个是新款数电发票。下面,我们重点分析一下属性对应的内容及含义,他们的信息以逗号为分隔符:
这里第一个固定属性值 01,第二个属性值 10 则是发票类型,具体含义如下:
后面几个分别是发票的代码、号码、金额、日期和校验码等信息。
4. 使用 .NET 处理 PDF 文件和二维码解析
了解了发票中二维码的信息后,我们可以使用 .NET 技术来处理 PDF 文件并进行二维码解析。在这里,我们将使用 UglyToad.PdfPig
库来读取 PDF 文件,使用 ZXing
库来解析二维码。
4.1. 准备工作
在开始之前,请确保你已经安装了以下 NuGet 包:
oUglyToad.PdfPig
oZXing.Net
你可以通过以下命令安装这些包:
dotnet add package PdfPig
dotnet 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 + P
SendKeys.SendWait("^(p)");
}
6. 总结
通过以上代码,我们展示了如何使用 .NET 结合 UglyToad.PdfPig
和 ZXing
库从 PDF 文件获取图片,并解析二维码信息,同时介绍了如何提取发票的其他信息,如日期、号码、类目和金额等。最后,我们还展示了如何将提取的发票信息导出为 CSV 文件,以及如何合并和打印 PDF 文件。希望这篇文章能帮助你更好地理解和实现发票信息的提取和处理。如果你有任何问题或建议,欢迎在评论区留言