mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-07 20:10:14 +08:00
351 lines
12 KiB
Go
351 lines
12 KiB
Go
|
|
package table
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
pdf "ragflow/internal/deepdoc/parser/pdf/type"
|
|
)
|
|
|
|
func TestGroupBoxesByRC_RDiffSplitsRows(t *testing.T) {
|
|
// 6 boxes with 6 different R values → 6 rows (Python R-first splitting).
|
|
boxes := []pdf.TextBox{
|
|
{X0: 10, X1: 90, Top: 0, Bottom: 30, Text: "A", R: 0, C: 0},
|
|
{X0: 110, X1: 190, Top: 0, Bottom: 30, Text: "B", R: 1, C: 1},
|
|
{X0: 210, X1: 290, Top: 0, Bottom: 30, Text: "C", R: 2, C: 2},
|
|
{X0: 10, X1: 90, Top: 35, Bottom: 65, Text: "D", R: 3, C: 0},
|
|
{X0: 110, X1: 190, Top: 35, Bottom: 65, Text: "E", R: 4, C: 1},
|
|
{X0: 210, X1: 290, Top: 35, Bottom: 65, Text: "F", R: 5, C: 2},
|
|
}
|
|
rows := GroupBoxesByRC(boxes)
|
|
// R=0,1,2,3,4,5 → 6 rows (Python: R differs → new row).
|
|
if len(rows) != 6 {
|
|
t.Fatalf("expected 6 rows (R differs → split), got %d", len(rows))
|
|
}
|
|
}
|
|
|
|
func TestGroupBoxesByRC_MergesCloseCols(t *testing.T) {
|
|
// R=0 has C=0,1. R=1 has C=0,1. C compression → 2 cols each.
|
|
boxes := []pdf.TextBox{
|
|
{X0: 10, X1: 90, Top: 0, Bottom: 30, Text: "A", R: 0, C: 0},
|
|
{X0: 110, X1: 190, Top: 0, Bottom: 30, Text: "B", R: 0, C: 1},
|
|
{X0: 10, X1: 90, Top: 35, Bottom: 65, Text: "C", R: 1, C: 0},
|
|
{X0: 110, X1: 190, Top: 35, Bottom: 65, Text: "D", R: 1, C: 1},
|
|
}
|
|
rows := GroupBoxesByRC(boxes)
|
|
if len(rows) != 2 {
|
|
t.Fatalf("expected 2 rows (R diff), got %d", len(rows))
|
|
}
|
|
if len(rows[0]) != 2 || len(rows[1]) != 2 {
|
|
t.Errorf("expected 2 cols/row, got %d/%d", len(rows[0]), len(rows[1]))
|
|
}
|
|
if rows[0][0].Text != "A" || rows[0][1].Text != "B" {
|
|
t.Errorf("row0 wrong: %q %q", rows[0][0].Text, rows[0][1].Text)
|
|
}
|
|
if rows[1][0].Text != "C" || rows[1][1].Text != "D" {
|
|
t.Errorf("row1 wrong: %q %q", rows[1][0].Text, rows[1][1].Text)
|
|
}
|
|
}
|
|
|
|
func TestGroupBoxesByRC_RDiffSplitsRow(t *testing.T) {
|
|
// R=0 and R=1 at same Y (overlapping) → two separate rows in the grid.
|
|
boxes := []pdf.TextBox{
|
|
{X0: 10, X1: 90, Top: 0, Bottom: 30, Text: "A", R: 0, C: 0},
|
|
{X0: 110, X1: 190, Top: 0, Bottom: 30, Text: "B", R: 1, C: 1},
|
|
{X0: 10, X1: 90, Top: 35, Bottom: 65, Text: "C", R: 2, C: 0},
|
|
{X0: 110, X1: 190, Top: 35, Bottom: 65, Text: "D", R: 3, C: 1},
|
|
}
|
|
rows := GroupBoxesByRC(boxes)
|
|
// R=0,1,2,3 → 4 different R values → 4 rows (Python: R differs → new row).
|
|
if len(rows) != 4 {
|
|
t.Fatalf("expected 4 rows (R differs → split), got %d", len(rows))
|
|
}
|
|
if rows[0][0].Text != "A" || rows[1][0].Text != "B" {
|
|
t.Errorf("row0/1 wrong: A=%q B=%q", rows[0][0].Text, rows[1][0].Text)
|
|
}
|
|
}
|
|
|
|
func TestFillCellTextFromBoxes_RCOnly(t *testing.T) {
|
|
cells := []pdf.TSRCell{
|
|
{X0: 0, Y0: 0, X1: 100, Y1: 50, Label: "table"},
|
|
{X0: 90, Y0: 0, X1: 200, Y1: 50, Label: "table"},
|
|
}
|
|
// This box straddles cell 0 (X=0-100) and cell 1 (X=90-200).
|
|
// Spatial overlap: both match. R/C: should go to cell R=0, C=0 only.
|
|
boxes := []pdf.TextBox{
|
|
{X0: 80, X1: 120, Top: 0, Bottom: 50, Text: "TEXT", LayoutType: "table", R: 0, C: 0},
|
|
}
|
|
rows := GroupTSRCellsToRows(cells)
|
|
for _, b := range boxes {
|
|
t := strings.TrimSpace(b.Text)
|
|
if t == "" {
|
|
continue
|
|
}
|
|
if b.R >= 0 && b.R < len(rows) && b.C >= 0 && b.C < len(rows[b.R]) {
|
|
rows[b.R][b.C].Text = t
|
|
}
|
|
}
|
|
// Cell 0 should have text, cell 1 should NOT.
|
|
if rows[0][0].Text != "TEXT" {
|
|
t.Errorf("cell[0][0] = %q, want %q", rows[0][0].Text, "TEXT")
|
|
}
|
|
if rows[0][1].Text != "" {
|
|
t.Errorf("cell[0][1] = %q, should be empty (spatial overlap leak)", rows[0][1].Text)
|
|
}
|
|
}
|
|
|
|
func TestGroupBoxesByRC_FallbackToYXWhenNoAnnotations(t *testing.T) {
|
|
// When all boxes have R=-1 (Python's case: regex didn't match "table" label),
|
|
// groupBoxesByRC should fall back to YX coordinate grouping.
|
|
boxes := []pdf.TextBox{
|
|
{X0: 10, X1: 90, Top: 0, Bottom: 30, Text: "A", R: -1, C: -1},
|
|
{X0: 110, X1: 190, Top: 0, Bottom: 30, Text: "B", R: -1, C: -1},
|
|
{X0: 10, X1: 90, Top: 35, Bottom: 65, Text: "C", R: -1, C: -1},
|
|
{X0: 110, X1: 190, Top: 35, Bottom: 65, Text: "D", R: -1, C: -1},
|
|
}
|
|
rows := GroupBoxesByRC(boxes)
|
|
// R=-1 for all → maxR = -1 → grid would be 0 rows. Must fall back to YX.
|
|
if len(rows) == 0 {
|
|
t.Fatal("groupBoxesByRC returned 0 rows when R=-1 — no YX fallback")
|
|
}
|
|
if len(rows) != 2 {
|
|
t.Errorf("expected 2 rows (Y-split), got %d", len(rows))
|
|
}
|
|
}
|
|
|
|
func TestGroupBoxesByRC_ColspanMissing(t *testing.T) {
|
|
// Box with SP annotation spanning 2 columns (HLeft→HRight covers cols 0-1).
|
|
boxes := []pdf.TextBox{
|
|
{X0: 10, X1: 90, Top: 0, Bottom: 30, Text: "Name", R: 0, C: 0, H: 1,
|
|
HLeft: 10, HRight: 200},
|
|
{X0: 110, X1: 200, Top: 0, Bottom: 30, Text: "", R: 0, C: 1, SP: 1},
|
|
{X0: 10, X1: 90, Top: 35, Bottom: 65, Text: "A", R: 1, C: 0},
|
|
{X0: 110, X1: 200, Top: 35, Bottom: 65, Text: "B", R: 1, C: 1},
|
|
}
|
|
rows := GroupBoxesByRC(boxes)
|
|
// The result should have colspan=2 for cell [0,0] and skip [0,1].
|
|
// Currently groupBoxesByRC produces a flat grid without span info.
|
|
if len(rows) >= 1 && len(rows[0]) >= 2 && rows[0][1].Text == "" {
|
|
t.Log("KNOWN LIMITATION: colspan not computed — cell [0,1] is empty instead of merged")
|
|
}
|
|
_ = rows
|
|
}
|
|
|
|
func TestCompressRowIndices(t *testing.T) {
|
|
// 6 boxes with 6 different R values → 6 rows.
|
|
boxes := []pdf.TextBox{
|
|
{X0: 10, X1: 90, Top: 0, Bottom: 30, Text: "A", R: 0, C: 0},
|
|
{X0: 110, X1: 190, Top: 0, Bottom: 30, Text: "B", R: 1, C: 1},
|
|
{X0: 210, X1: 290, Top: 0, Bottom: 30, Text: "C", R: 2, C: 2},
|
|
{X0: 10, X1: 90, Top: 35, Bottom: 65, Text: "D", R: 3, C: 0},
|
|
{X0: 110, X1: 190, Top: 35, Bottom: 65, Text: "E", R: 4, C: 1},
|
|
{X0: 210, X1: 290, Top: 35, Bottom: 65, Text: "F", R: 5, C: 2},
|
|
}
|
|
// Sort first (as GroupBoxesByRC does)
|
|
sort.Slice(boxes, func(i, j int) bool {
|
|
if boxes[i].R != boxes[j].R {
|
|
return boxes[i].R < boxes[j].R
|
|
}
|
|
if boxes[i].Top != boxes[j].Top {
|
|
return boxes[i].Top < boxes[j].Top
|
|
}
|
|
return boxes[i].X0 < boxes[j].X0
|
|
})
|
|
|
|
rowMap, compressed := compressRowIndices(boxes)
|
|
if compressed != 5 { // 0-based, 6 elements → max index 5
|
|
t.Errorf("compressed = %d, want 5", compressed)
|
|
}
|
|
if rowMap[0] != 0 || rowMap[1] != 1 || rowMap[2] != 2 || rowMap[3] != 3 || rowMap[4] != 4 || rowMap[5] != 5 {
|
|
t.Errorf("rowMap mapping incorrect: %v", rowMap)
|
|
}
|
|
}
|
|
|
|
func TestCompressRowIndices_SameR(t *testing.T) {
|
|
// Multiple boxes with same R → same compressed row.
|
|
boxes := []pdf.TextBox{
|
|
{X0: 10, X1: 90, Top: 0, Bottom: 30, Text: "A", R: 0, C: 0},
|
|
{X0: 110, X1: 190, Top: 0, Bottom: 30, Text: "B", R: 0, C: 1},
|
|
{X0: 10, X1: 90, Top: 35, Bottom: 65, Text: "C", R: 1, C: 0},
|
|
{X0: 110, X1: 190, Top: 35, Bottom: 65, Text: "D", R: 1, C: 1},
|
|
}
|
|
// Sort first
|
|
sort.Slice(boxes, func(i, j int) bool {
|
|
if boxes[i].R != boxes[j].R {
|
|
return boxes[i].R < boxes[j].R
|
|
}
|
|
if boxes[i].Top != boxes[j].Top {
|
|
return boxes[i].Top < boxes[j].Top
|
|
}
|
|
return boxes[i].X0 < boxes[j].X0
|
|
})
|
|
|
|
rowMap, compressed := compressRowIndices(boxes)
|
|
if compressed != 1 { // 0-based, 2 rows → max index 1
|
|
t.Errorf("compressed = %d, want 1", compressed)
|
|
}
|
|
if rowMap[0] != 0 || rowMap[1] != 1 {
|
|
t.Errorf("rowMap mapping incorrect: %v", rowMap)
|
|
}
|
|
}
|
|
|
|
func TestCollectBoxesPerRow(t *testing.T) {
|
|
boxes := []pdf.TextBox{
|
|
{X0: 10, X1: 90, Top: 0, Bottom: 30, Text: "A", R: 0, C: 0},
|
|
{X0: 110, X1: 190, Top: 0, Bottom: 30, Text: "B", R: 0, C: 1},
|
|
{X0: 10, X1: 90, Top: 35, Bottom: 65, Text: "C", R: 1, C: 0},
|
|
{X0: 110, X1: 190, Top: 35, Bottom: 65, Text: "D", R: 1, C: 1},
|
|
}
|
|
// Sort first
|
|
sort.Slice(boxes, func(i, j int) bool {
|
|
if boxes[i].R != boxes[j].R {
|
|
return boxes[i].R < boxes[j].R
|
|
}
|
|
if boxes[i].Top != boxes[j].Top {
|
|
return boxes[i].Top < boxes[j].Top
|
|
}
|
|
return boxes[i].X0 < boxes[j].X0
|
|
})
|
|
|
|
rowMap, _ := compressRowIndices(boxes)
|
|
cmap, maxCols := collectBoxesPerRow(boxes, rowMap)
|
|
|
|
if len(cmap) != 2 {
|
|
t.Errorf("cmap has %d rows, want 2", len(cmap))
|
|
}
|
|
if cmap[0][0].txt != "A" || cmap[0][1].txt != "B" {
|
|
t.Errorf("row 0 incorrect: %v, %v", cmap[0][0], cmap[0][1])
|
|
}
|
|
if cmap[1][0].txt != "C" || cmap[1][1].txt != "D" {
|
|
t.Errorf("row 1 incorrect: %v, %v", cmap[1][0], cmap[1][1])
|
|
}
|
|
if maxCols[0] != 1 || maxCols[1] != 1 {
|
|
t.Errorf("maxCols incorrect: %v", maxCols)
|
|
}
|
|
}
|
|
|
|
func TestCollectBoxesPerRow_MergeSameCell(t *testing.T) {
|
|
boxes := []pdf.TextBox{
|
|
{X0: 10, X1: 50, Top: 0, Bottom: 30, Text: "Hello", R: 0, C: 0},
|
|
{X0: 50, X1: 90, Top: 0, Bottom: 30, Text: "World", R: 0, C: 0},
|
|
}
|
|
// Sort first
|
|
sort.Slice(boxes, func(i, j int) bool {
|
|
if boxes[i].R != boxes[j].R {
|
|
return boxes[i].R < boxes[j].R
|
|
}
|
|
if boxes[i].Top != boxes[j].Top {
|
|
return boxes[i].Top < boxes[j].Top
|
|
}
|
|
return boxes[i].X0 < boxes[j].X0
|
|
})
|
|
|
|
rowMap, _ := compressRowIndices(boxes)
|
|
cmap, _ := collectBoxesPerRow(boxes, rowMap)
|
|
|
|
if cmap[0][0].txt != "Hello World" {
|
|
t.Errorf("merged text incorrect: %q", cmap[0][0].txt)
|
|
}
|
|
}
|
|
|
|
func TestCompressColIndices(t *testing.T) {
|
|
boxes := []pdf.TextBox{
|
|
{X0: 10, X1: 90, Top: 0, Bottom: 30, Text: "A", R: 0, C: 0},
|
|
{X0: 110, X1: 190, Top: 0, Bottom: 30, Text: "B", R: 0, C: 1},
|
|
{X0: 10, X1: 90, Top: 35, Bottom: 65, Text: "C", R: 1, C: 0},
|
|
{X0: 110, X1: 190, Top: 35, Bottom: 65, Text: "D", R: 1, C: 1},
|
|
}
|
|
// Sort first
|
|
sort.Slice(boxes, func(i, j int) bool {
|
|
if boxes[i].R != boxes[j].R {
|
|
return boxes[i].R < boxes[j].R
|
|
}
|
|
if boxes[i].Top != boxes[j].Top {
|
|
return boxes[i].Top < boxes[j].Top
|
|
}
|
|
return boxes[i].X0 < boxes[j].X0
|
|
})
|
|
|
|
rowMap, compressed := compressRowIndices(boxes)
|
|
cCompressed, cMaxCol := compressColIndices(boxes, rowMap, compressed)
|
|
|
|
if cCompressed[0][0] != 0 || cCompressed[0][1] != 1 {
|
|
t.Errorf("row 0 compression incorrect: %v", cCompressed[0])
|
|
}
|
|
if cCompressed[1][0] != 0 || cCompressed[1][1] != 1 {
|
|
t.Errorf("row 1 compression incorrect: %v", cCompressed[1])
|
|
}
|
|
if cMaxCol[0] != 1 || cMaxCol[1] != 1 {
|
|
t.Errorf("cMaxCol incorrect: %v", cMaxCol)
|
|
}
|
|
}
|
|
|
|
func TestCompressColIndices_OverlapMerge(t *testing.T) {
|
|
boxes := []pdf.TextBox{
|
|
{X0: 10, X1: 100, Top: 0, Bottom: 30, Text: "A", R: 0, C: 0},
|
|
{X0: 90, X1: 190, Top: 0, Bottom: 30, Text: "B", R: 0, C: 1}, // Overlaps with A
|
|
}
|
|
// Sort first
|
|
sort.Slice(boxes, func(i, j int) bool {
|
|
if boxes[i].R != boxes[j].R {
|
|
return boxes[i].R < boxes[j].R
|
|
}
|
|
if boxes[i].Top != boxes[j].Top {
|
|
return boxes[i].Top < boxes[j].Top
|
|
}
|
|
return boxes[i].X0 < boxes[j].X0
|
|
})
|
|
|
|
rowMap, compressed := compressRowIndices(boxes)
|
|
cCompressed, cMaxCol := compressColIndices(boxes, rowMap, compressed)
|
|
|
|
if cCompressed[0][0] != 0 || cCompressed[0][1] != 0 {
|
|
t.Errorf("overlap merge incorrect: %v", cCompressed[0])
|
|
}
|
|
// After fix: cMaxCol tracks unique compressed columns, not original C keys.
|
|
// 2 overlapping boxes → 1 compressed column → cMaxCol[0] == 0
|
|
if cMaxCol[0] != 0 {
|
|
t.Errorf("cMaxCol incorrect: got %d, want 0 (2 overlapping boxes → 1 compressed column)", cMaxCol[0])
|
|
}
|
|
}
|
|
|
|
func TestBuildGrid(t *testing.T) {
|
|
boxes := []pdf.TextBox{
|
|
{X0: 10, X1: 90, Top: 0, Bottom: 30, Text: "A", R: 0, C: 0},
|
|
{X0: 110, X1: 190, Top: 0, Bottom: 30, Text: "B", R: 0, C: 1},
|
|
{X0: 10, X1: 90, Top: 35, Bottom: 65, Text: "C", R: 1, C: 0},
|
|
{X0: 110, X1: 190, Top: 35, Bottom: 65, Text: "D", R: 1, C: 1},
|
|
}
|
|
// Sort first
|
|
sort.Slice(boxes, func(i, j int) bool {
|
|
if boxes[i].R != boxes[j].R {
|
|
return boxes[i].R < boxes[j].R
|
|
}
|
|
if boxes[i].Top != boxes[j].Top {
|
|
return boxes[i].Top < boxes[j].Top
|
|
}
|
|
return boxes[i].X0 < boxes[j].X0
|
|
})
|
|
|
|
rowMap, compressed := compressRowIndices(boxes)
|
|
cmap, _ := collectBoxesPerRow(boxes, rowMap)
|
|
cCompressed, cMaxCol := compressColIndices(boxes, rowMap, compressed)
|
|
|
|
rows := buildGrid(cmap, cCompressed, cMaxCol, compressed)
|
|
|
|
if len(rows) != 2 {
|
|
t.Fatalf("expected 2 rows, got %d", len(rows))
|
|
}
|
|
if len(rows[0]) != 2 || len(rows[1]) != 2 {
|
|
t.Errorf("expected 2 cols each, got %d and %d", len(rows[0]), len(rows[1]))
|
|
}
|
|
if rows[0][0].Text != "A" || rows[0][1].Text != "B" {
|
|
t.Errorf("row 0 wrong: %q %q", rows[0][0].Text, rows[0][1].Text)
|
|
}
|
|
if rows[1][0].Text != "C" || rows[1][1].Text != "D" {
|
|
t.Errorf("row 1 wrong: %q %q", rows[1][0].Text, rows[1][1].Text)
|
|
}
|
|
}
|