/
afanasevn
/
PdfEncoder
Обзор
Документация
Войти
/
afanasevn
/
PdfEncoder
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/PdfEncoder.Infrastructure/Pdf/TableHeuristicExtractor.cs
124 строки
3 KB
IBS\NAfanasev
DocumentContext local functional
01 июл 2026, 15:30
01 июл 2026, 15:30
b4038c9
Код
Авторство
О чём код?
using PdfEncoder.Domain.Models; using UglyToad.PdfPig.Content; using UglyToad.PdfPig.DocumentLayoutAnalysis.WordExtractor; namespace PdfEncoder.Infrastructure.Pdf; /// <summary> /// Эвристическое извлечение таблиц по выравниванию колонок. /// </summary> public static class TableHeuristicExtractor { private const double LineTolerance = 4.0; private const double ColumnGapThreshold = 20.0; /// <summary> /// Извлекает таблицы со страницы по whitespace/column alignment. /// </summary> public static IReadOnlyList<ExtractedTable> ExtractTables( Page page, int pageNumber) { var words = page.GetWords(NearestNeighbourWordExtractor.Instance).ToList(); if (words.Count == 0) { return []; } var lines = GroupWordsIntoLines(words); if (lines.Count < 2) { return []; } var rowCandidates = lines .Select(line => SplitLineIntoColumns(line)) .Where(columns => columns.Count >= 2) .ToList(); if (rowCandidates.Count < 2) { return []; } var columnCount = rowCandidates.Max(row => row.Count); var normalizedRows = rowCandidates .Where(row => row.Count == columnCount) .ToList(); if (normalizedRows.Count < 2) { return []; } var headers = normalizedRows[0]; var rows = normalizedRows.Skip(1).ToList(); return [ new ExtractedTable { PageNumber = pageNumber, Headers = headers, Rows = rows, Confidence = 0.5 } ]; } private static List<List<Word>> GroupWordsIntoLines(IReadOnlyList<Word> words) { var sorted = words.OrderByDescending(word => word.BoundingBox.Bottom).ToList(); var lines = new List<List<Word>>(); List<Word>? currentLine = null; double? currentBaseline = null; foreach (var word in sorted) { if (currentLine is null || currentBaseline is null || Math.Abs(word.BoundingBox.Bottom - currentBaseline.Value) > LineTolerance) { currentLine = [word]; lines.Add(currentLine); currentBaseline = word.BoundingBox.Bottom; continue; } currentLine.Add(word); } foreach (var line in lines) { line.Sort((left, right) => left.BoundingBox.Left.CompareTo(right.BoundingBox.Left)); } return lines; } private static List<string> SplitLineIntoColumns(IReadOnlyList<Word> lineWords) { if (lineWords.Count == 0) { return []; } var columns = new List<string>(); var current = lineWords[0].Text; for (var i = 1; i < lineWords.Count; i++) { var gap = lineWords[i].BoundingBox.Left - lineWords[i - 1].BoundingBox.Right; if (gap >= ColumnGapThreshold) { columns.Add(current.Trim()); current = lineWords[i].Text; } else { current += " " + lineWords[i].Text; } } columns.Add(current.Trim()); return columns; } }