如何使用ASP.NET Core与PdfPig去除PDF发票重复页脚
解决PdfPig提取发票PDF时重复页脚的问题
问题1:页脚仅保留一次或单独提取的方案
PdfPig本身没有直接识别、过滤页脚的内置功能,需要通过提取时的位置过滤或提取后的文本后处理来实现:
- 提取时过滤:识别每页底部的页脚区域,只保留最后一页的页脚,其余页面的页脚直接跳过;
- 提取后处理:先提取所有页面的完整文本,再通过内容匹配找出重复的页脚内容,仅保留最后一次出现的版本,或者单独提取所有页脚内容合并去重。
问题2:判断WordElement是否为页脚的方法
可以通过位置判断和内容匹配两种方式实现,结合C#文本处理或正则即可完成过滤:
方法1:基于页面位置判断(最可靠)
页脚通常固定在页面底部的特定区域,可以通过Word的BoundingBox计算相对页面的位置:
- 获取页面的总高度:
page.Height; - 定义页脚区域的阈值,比如页面底部10%的区域(可根据实际发票调整);
- 判断Word的
BoundingBox.Bottom是否小于等于页面高度的10%(PDF坐标原点在左下角,Bottom值越小越靠近底部)。
方法2:基于内容正则匹配
如果页脚有固定格式(比如包含发票号、公司名称、固定标语等),可以编写对应正则表达式匹配内容,标记并过滤这类重复内容。
修改后的完整代码示例
以下代码结合位置判断实现提取时过滤中间页的页脚,仅保留最后一页的页脚:
async Task<string> ExtractTextPdfPigAsync(MemoryStream pdfStream) { pdfStream.Position = 0; StringBuilder tulem = new(); using UglyToad.PdfPig.PdfDocument document = UglyToad.PdfPig.PdfDocument.Open(pdfStream, new UglyToad.PdfPig.ParsingOptions() { SkipMissingFonts = true }); var pages = document.GetPages().ToList(); // 遍历所有页面,最后一页单独处理保留页脚 for (int i = 0; i < pages.Count; i++) { var page = pages[i]; // 仅最后一页保留页脚,其余页面提取时过滤页脚 GetWordsInReadingOrder(page, tulem, isLastPage: i == pages.Count - 1); // 页面间添加换行分隔 if (i != pages.Count - 1) { tulem.AppendLine(); tulem.AppendLine("---"); } } return tulem.ToString(); } /// <summary> /// 按阅读顺序提取页面文本,可选择是否保留页脚 /// </summary> /// <param name="page">目标页面</param> /// <param name="builder">文本构建器</param> /// <param name="isLastPage">是否为最后一页(保留页脚)</param> public static void GetWordsInReadingOrder(Page page, StringBuilder builder, bool isLastPage = false) { List<Word> words = page.GetWords().ToList(); if (words.Count == 0) return; // 过滤页脚:如果不是最后一页,移除页面底部10%区域的文字 if (!isLastPage) { double footerThreshold = page.Height * 0.1; // 底部10%区域定义为页脚 words = words.Where(w => w.BoundingBox.Bottom > footerThreshold).ToList(); if (words.Count == 0) return; } List<Word> sorted = words.OrderBy(r => r.BoundingBox.Bottom).ThenBy(r => r.BoundingBox.Left).ToList(); List<WordsWithLines> lines = []; const double rowHeight = 10; double lnLastTop = 0; int lnLastRow = 1; double csvReaVpos = sorted.First().BoundingBox.Bottom; foreach (Word r in sorted) { if (r.BoundingBox.Bottom >= lnLastTop + (0.5 * rowHeight) || r.BoundingBox.Bottom - csvReaVpos > rowHeight) { ++lnLastRow; csvReaVpos = r.BoundingBox.Bottom; } lnLastTop = r.BoundingBox.Bottom; lines.Add(new WordsWithLines() { RowNo = lnLastRow, WordElement = r }); } IOrderedEnumerable<WordsWithLines> wordsList = lines.OrderByDescending(r => r.RowNo).ThenBy(r => r.WordElement.BoundingBox.Left); int rowNo = wordsList.First().RowNo; bool first = true; foreach (WordsWithLines word in wordsList) { if (rowNo != word.RowNo) { builder.AppendLine(); first = true; rowNo = word.RowNo; } if (!first) builder.Append(' '); builder.Append(word.WordElement.Text); first = false; } } sealed class WordsWithLines { public int RowNo; public Word WordElement; }
正则去重的补充方案
如果需要提取后通过内容去重,可以在提取完成后添加以下逻辑:
// 假设页脚正则为匹配固定格式的内容,比如包含"公司名称"和"发票号"的行 string footerPattern = @"^.*公司名称.*发票号:\d+.*$"; Regex footerRegex = new Regex(footerPattern, RegexOptions.Multiline | RegexOptions.IgnoreCase); string fullText = tulem.ToString(); // 找出所有页脚匹配项 var matches = footerRegex.Matches(fullText).Cast<Match>().Select(m => m.Value).Distinct().ToList(); // 移除所有页脚,再添加一次唯一的页脚 string cleanedText = footerRegex.Replace(fullText, ""); if (matches.Any()) { cleanedText += Environment.NewLine + Environment.NewLine + matches.First(); }
内容的提问来源于stack exchange,提问作者Andrus
相关产品推荐
相关产品推荐

