mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-25 01:20:11 +08:00
523 lines
19 KiB
Go
523 lines
19 KiB
Go
package table
|
||
|
||
import (
|
||
"fmt"
|
||
"testing"
|
||
|
||
pdf "ragflow/internal/deepdoc/parser/pdf/type"
|
||
)
|
||
|
||
func TestCrossPageTableMerge(t *testing.T) {
|
||
// Page 0 table: 2 cells, positioned at page 0.
|
||
pg0 := pdf.TableItem{
|
||
Positions: []pdf.Position{
|
||
{PageNumbers: []int{0}, Left: 50, Right: 500, Top: 100, Bottom: 800},
|
||
},
|
||
Scale: 1.0,
|
||
Cells: []pdf.TSRCell{
|
||
{X0: 0, Y0: 0, X1: 100, Y1: 50, Text: "pg0_r0c0"},
|
||
{X0: 100, Y0: 0, X1: 200, Y1: 50, Text: "pg0_r0c1"},
|
||
},
|
||
}
|
||
// Page 1 table: 2 cells, same X range, positioned at page 1.
|
||
pg1 := pdf.TableItem{
|
||
Positions: []pdf.Position{
|
||
{PageNumbers: []int{1}, Left: 50, Right: 500, Top: 100, Bottom: 300},
|
||
},
|
||
Scale: 1.0,
|
||
Cells: []pdf.TSRCell{
|
||
{X0: 0, Y0: 0, X1: 100, Y1: 50, Text: "pg1_r0c0"},
|
||
{X0: 100, Y0: 0, X1: 200, Y1: 50, Text: "pg1_r0c1"},
|
||
},
|
||
}
|
||
tables := []pdf.TableItem{pg0, pg1}
|
||
|
||
// mergeTablesAcrossPages merges tables on consecutive pages with X overlap.
|
||
merged := MergeTablesAcrossPages(tables, nil)
|
||
if len(merged) != 1 {
|
||
t.Fatalf("expected 1 merged table, got %d", len(merged))
|
||
}
|
||
if len(merged[0].Cells) != 4 {
|
||
t.Errorf("expected 4 merged cells, got %d", len(merged[0].Cells))
|
||
}
|
||
if len(merged[0].Positions) != 2 {
|
||
t.Errorf("expected 2 merged positions, got %d", len(merged[0].Positions))
|
||
}
|
||
t.Logf("Merged %d cells across %d pages", len(merged[0].Cells), len(merged[0].Positions))
|
||
}
|
||
|
||
// TestMergeTablesAcrossPages_NoOverlap verifies that non-adjacent or
|
||
// non-overlapping tables are NOT merged.
|
||
func TestMergeTablesAcrossPages_NoOverlap(t *testing.T) {
|
||
// Tables with no X overlap should NOT be merged.
|
||
tables := []pdf.TableItem{
|
||
{
|
||
Positions: []pdf.Position{{PageNumbers: []int{0}, Left: 50, Right: 100, Top: 100, Bottom: 500}},
|
||
Scale: 1.0,
|
||
Cells: []pdf.TSRCell{{Text: "left"}},
|
||
},
|
||
{
|
||
Positions: []pdf.Position{{PageNumbers: []int{1}, Left: 500, Right: 600, Top: 100, Bottom: 500}},
|
||
Scale: 1.0,
|
||
Cells: []pdf.TSRCell{{Text: "right"}},
|
||
},
|
||
}
|
||
merged := MergeTablesAcrossPages(tables, nil)
|
||
if len(merged) != 2 {
|
||
t.Fatalf("non-overlapping tables: expected 2 tables, got %d", len(merged))
|
||
}
|
||
}
|
||
|
||
// TestMergeTablesAcrossPages_NonConsecutive verifies that tables on
|
||
// non-consecutive pages are NOT merged.
|
||
func TestMergeTablesAcrossPages_NonConsecutive(t *testing.T) {
|
||
tables := []pdf.TableItem{
|
||
{
|
||
Positions: []pdf.Position{{PageNumbers: []int{0}, Left: 50, Right: 500, Top: 100, Bottom: 500}},
|
||
Scale: 1.0,
|
||
Cells: []pdf.TSRCell{{Text: "page0"}},
|
||
},
|
||
{
|
||
Positions: []pdf.Position{{PageNumbers: []int{3}, Left: 50, Right: 500, Top: 100, Bottom: 500}},
|
||
Scale: 1.0,
|
||
Cells: []pdf.TSRCell{{Text: "page3"}},
|
||
},
|
||
}
|
||
merged := MergeTablesAcrossPages(tables, nil)
|
||
if len(merged) != 2 {
|
||
t.Fatalf("non-consecutive pages: expected 2 tables, got %d", len(merged))
|
||
}
|
||
}
|
||
|
||
// TestMergeTablesAcrossPages_SingleTable verifies that a single table
|
||
// passes through unchanged.
|
||
func TestMergeTablesAcrossPages_SingleTable(t *testing.T) {
|
||
tables := []pdf.TableItem{
|
||
{
|
||
Positions: []pdf.Position{{PageNumbers: []int{0}, Left: 50, Right: 500, Top: 100, Bottom: 500}},
|
||
Scale: 1.0,
|
||
Cells: []pdf.TSRCell{{Text: "only"}},
|
||
},
|
||
}
|
||
merged := MergeTablesAcrossPages(tables, nil)
|
||
if len(merged) != 1 {
|
||
t.Fatalf("single table: expected 1 table, got %d", len(merged))
|
||
}
|
||
}
|
||
|
||
func TestMergeTablesAcrossPages_EmptyPositions(t *testing.T) {
|
||
// Tables with empty Positions should be preserved (not dropped).
|
||
tables := []pdf.TableItem{
|
||
{
|
||
Positions: []pdf.Position{},
|
||
Cells: []pdf.TSRCell{{Text: "posless"}},
|
||
},
|
||
{
|
||
Positions: []pdf.Position{{PageNumbers: []int{0}, Left: 50, Right: 500, Top: 100, Bottom: 500}},
|
||
Scale: 1.0,
|
||
Cells: []pdf.TSRCell{{Text: "normal"}},
|
||
},
|
||
}
|
||
merged := MergeTablesAcrossPages(tables, nil)
|
||
if len(merged) != 2 {
|
||
t.Fatalf("empty Positions: expected 2 tables (preserved), got %d", len(merged))
|
||
}
|
||
// Tables with Positions come first (from items list), positionless tables are appended.
|
||
if len(merged[0].Positions) == 0 {
|
||
t.Error("expected table with Positions first in result")
|
||
}
|
||
if len(merged[1].Positions) != 0 {
|
||
t.Error("expected positionless table second in result")
|
||
}
|
||
if merged[1].Cells[0].Text != "posless" {
|
||
t.Errorf("positionless table content lost: got %q", merged[1].Cells[0].Text)
|
||
}
|
||
}
|
||
|
||
func TestMergeTablesAcrossPages_LargeYGap(t *testing.T) {
|
||
// Tables with large Y gap should NOT be merged.
|
||
medianHeights := map[int]float64{0: 10}
|
||
tables := []pdf.TableItem{
|
||
{
|
||
Positions: []pdf.Position{{PageNumbers: []int{0}, Left: 50, Right: 500, Top: 100, Bottom: 150}},
|
||
Scale: 1.0,
|
||
Cells: []pdf.TSRCell{{Text: "page0"}},
|
||
},
|
||
{
|
||
Positions: []pdf.Position{{PageNumbers: []int{1}, Left: 50, Right: 500, Top: 5000, Bottom: 5100}},
|
||
Scale: 1.0,
|
||
Cells: []pdf.TSRCell{{Text: "page1_far"}},
|
||
},
|
||
}
|
||
merged := MergeTablesAcrossPages(tables, medianHeights)
|
||
if len(merged) != 2 {
|
||
t.Fatalf("large Y gap: expected 2 tables (not merged), got %d", len(merged))
|
||
}
|
||
}
|
||
|
||
func TestMergeTablesAcrossPages_NoMedianHeights(t *testing.T) {
|
||
// Without medianHeights, mh defaults to 10 and threshold is 230.
|
||
// yDis = (10 + 120 - 150 - 150) / 2 = -85, which is <= 230, so they merge.
|
||
tables := []pdf.TableItem{
|
||
{
|
||
Positions: []pdf.Position{{PageNumbers: []int{0}, Left: 50, Right: 500, Top: 100, Bottom: 150}},
|
||
Scale: 1.0,
|
||
Cells: []pdf.TSRCell{{Text: "page0"}},
|
||
},
|
||
{
|
||
Positions: []pdf.Position{{PageNumbers: []int{1}, Left: 50, Right: 500, Top: 10, Bottom: 120}},
|
||
Scale: 1.0,
|
||
Cells: []pdf.TSRCell{{Text: "page1_near"}},
|
||
},
|
||
}
|
||
merged := MergeTablesAcrossPages(tables, nil)
|
||
if len(merged) != 1 {
|
||
t.Fatalf("no medianHeights: expected 1 merged table, got %d", len(merged))
|
||
}
|
||
if len(merged[0].Cells) != 2 {
|
||
t.Errorf("expected 2 cells after merge, got %d", len(merged[0].Cells))
|
||
}
|
||
}
|
||
|
||
// TestMergeTablesAcrossPages_RebuildsGridAcrossPages verifies that after a
|
||
// cross-page merge the merged table's Grid contains rows from BOTH pages,
|
||
// not just the anchor (page-0) grid. This catches the regression where
|
||
// ConstructTable reads the stale anchor Grid and drops all continuation rows.
|
||
func TestMergeTablesAcrossPages_RebuildsGridAcrossPages(t *testing.T) {
|
||
pageGrid := func(rows [][]string) [][]pdf.TSRCell {
|
||
g := make([][]pdf.TSRCell, len(rows))
|
||
for r, row := range rows {
|
||
g[r] = make([]pdf.TSRCell, len(row))
|
||
for c := range row {
|
||
g[r][c] = pdf.TSRCell{
|
||
X0: float64(c) * 100, Y0: float64(r) * 30,
|
||
X1: float64(c)*100 + 100, Y1: float64(r)*30 + 30,
|
||
Text: row[c],
|
||
}
|
||
}
|
||
}
|
||
return g
|
||
}
|
||
pg0 := pdf.TableItem{
|
||
Positions: []pdf.Position{{PageNumbers: []int{0}, Left: 0, Right: 200, Top: 0, Bottom: 60}},
|
||
Scale: 1.0,
|
||
Grid: pageGrid([][]string{{"a", "b"}, {"c", "d"}}),
|
||
}
|
||
pg1 := pdf.TableItem{
|
||
Positions: []pdf.Position{{PageNumbers: []int{1}, Left: 0, Right: 200, Top: 0, Bottom: 60}},
|
||
Scale: 1.0,
|
||
Grid: pageGrid([][]string{{"e", "f"}, {"g", "h"}}),
|
||
}
|
||
|
||
merged := MergeTablesAcrossPages([]pdf.TableItem{pg0, pg1}, nil)
|
||
if len(merged) != 1 {
|
||
t.Fatalf("expected 1 merged table, got %d", len(merged))
|
||
}
|
||
// Anchor has 2 rows, continuation has 2 rows → merged Grid must be 4.
|
||
if len(merged[0].Grid) != 4 {
|
||
t.Fatalf("merged Grid must contain rows from both pages (want 4), got %d", len(merged[0].Grid))
|
||
}
|
||
// Continuation rows must appear after anchor rows, in page order.
|
||
if merged[0].Grid[0][0].Text != "a" || merged[0].Grid[2][0].Text != "e" {
|
||
t.Errorf("row order wrong after stacking: %s / %s", merged[0].Grid[0][0].Text, merged[0].Grid[2][0].Text)
|
||
}
|
||
// Continuation rows must be Y-shifted strictly below the anchor rows so
|
||
// Y-monotonic downstream logic (span detection, ordering) stays correct.
|
||
// Catches a regression where the shift is dropped but stacking is kept:
|
||
// row order would still be correct by page order, so only this assertion
|
||
// would fail.
|
||
if merged[0].Grid[2][0].Y0 <= merged[0].Grid[1][0].Y1 {
|
||
t.Errorf("continuation row was not shifted below the anchor rows (Grid[2][0].Y0=%v <= Grid[1][0].Y1=%v)",
|
||
merged[0].Grid[2][0].Y0, merged[0].Grid[1][0].Y1)
|
||
}
|
||
}
|
||
|
||
// TestMergeTablesAcrossPages_JaggedContinuationPreservesRows verifies that
|
||
// when a continuation page's grid is not column-uniform with the anchor (a
|
||
// jagged cross-page stack), MergeTablesAcrossPages still preserves every
|
||
// continuation row. It aligns both per-page grids to a shared column model
|
||
// (the max column count, padding shorter rows by index) and stacks all rows,
|
||
// instead of dropping the continuation page's Grid. Regression test for the
|
||
// bug where a non-uniform cross-page grid silently deleted an entire
|
||
// continuation page from the output.
|
||
func TestMergeTablesAcrossPages_JaggedContinuationPreservesRows(t *testing.T) {
|
||
pageGrid := func(rows [][]string) [][]pdf.TSRCell {
|
||
g := make([][]pdf.TSRCell, len(rows))
|
||
for r, row := range rows {
|
||
g[r] = make([]pdf.TSRCell, len(row))
|
||
for c := range row {
|
||
g[r][c] = pdf.TSRCell{
|
||
X0: float64(c) * 100, Y0: float64(r) * 30,
|
||
X1: float64(c)*100 + 100, Y1: float64(r)*30 + 30,
|
||
Text: row[c],
|
||
}
|
||
}
|
||
}
|
||
return g
|
||
}
|
||
cells := func(rows [][]string) []pdf.TSRCell {
|
||
var cs []pdf.TSRCell
|
||
for r, row := range rows {
|
||
for c := range row {
|
||
cs = append(cs, pdf.TSRCell{
|
||
X0: float64(c) * 100, Y0: float64(r) * 30,
|
||
X1: float64(c)*100 + 100, Y1: float64(r)*30 + 30,
|
||
Text: row[c],
|
||
})
|
||
}
|
||
}
|
||
return cs
|
||
}
|
||
cases := []struct {
|
||
name string
|
||
anchorRows [][]string
|
||
contRows [][]string
|
||
contCellText string
|
||
}{
|
||
{
|
||
name: "first row column count differs",
|
||
anchorRows: [][]string{{"a", "b", "c"}, {"d", "e", "f"}},
|
||
contRows: [][]string{{"g", "h"}, {"i", "j"}},
|
||
contCellText: "g",
|
||
},
|
||
{
|
||
name: "interior row column count differs",
|
||
anchorRows: [][]string{{"a", "b", "c"}, {"d", "e", "f"}},
|
||
contRows: [][]string{{"g", "h", "i"}, {"j", "k"}},
|
||
contCellText: "g",
|
||
},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
pg0 := pdf.TableItem{
|
||
Positions: []pdf.Position{{PageNumbers: []int{0}, Left: 0, Right: 300, Top: 0, Bottom: 60}},
|
||
Scale: 1.0,
|
||
Grid: pageGrid(tc.anchorRows),
|
||
Cells: cells(tc.anchorRows),
|
||
}
|
||
pg1 := pdf.TableItem{
|
||
Positions: []pdf.Position{{PageNumbers: []int{1}, Left: 0, Right: 300, Top: 0, Bottom: 60}},
|
||
Scale: 1.0,
|
||
Grid: pageGrid(tc.contRows),
|
||
Cells: cells(tc.contRows),
|
||
}
|
||
|
||
merged := MergeTablesAcrossPages([]pdf.TableItem{pg0, pg1}, nil)
|
||
if len(merged) != 1 {
|
||
t.Fatalf("expected 1 merged table, got %d", len(merged))
|
||
}
|
||
// Columns differ (anchor 3 vs continuation jagged) → aligned to
|
||
// uniCols=3, rows still stacked: anchor + continuation (no drop).
|
||
wantRows := len(tc.anchorRows) + len(tc.contRows)
|
||
if len(merged[0].Grid) != wantRows {
|
||
t.Fatalf("jagged continuation must preserve all rows (want %d), got %d", wantRows, len(merged[0].Grid))
|
||
}
|
||
// Aligned width is the max column count (3) for every row.
|
||
for r, row := range merged[0].Grid {
|
||
if len(row) != 3 {
|
||
t.Errorf("row %d: aligned grid width must be max cols (3), got %d", r, len(row))
|
||
}
|
||
}
|
||
// Anchor rows preserved first.
|
||
if merged[0].Grid[0][0].Text != tc.anchorRows[0][0] || merged[0].Grid[1][0].Text != tc.anchorRows[1][0] {
|
||
t.Errorf("anchor rows corrupted after alignment: %s / %s", merged[0].Grid[0][0].Text, merged[0].Grid[1][0].Text)
|
||
}
|
||
// Continuation rows appended in page order, padded by index.
|
||
base := len(tc.anchorRows)
|
||
for r, crow := range tc.contRows {
|
||
for c, txt := range crow {
|
||
if merged[0].Grid[base+r][c].Text != txt {
|
||
t.Errorf("continuation cell lost after alignment: Grid[%d][%d]=%q want %q", base+r, c, merged[0].Grid[base+r][c].Text, txt)
|
||
}
|
||
}
|
||
}
|
||
// Continuation Cells are still appended (merge decision unchanged).
|
||
hasCont := false
|
||
for _, c := range merged[0].Cells {
|
||
if c.Text == tc.contCellText {
|
||
hasCont = true
|
||
break
|
||
}
|
||
}
|
||
if !hasCont {
|
||
t.Errorf("continuation Cells should still be appended after alignment")
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestMergeTablesAcrossPages_MixedColumnCountsPreservesAllRows reproduces the
|
||
// 中加纯债 cross-page table: page 0 has 27 rows × 6 cols and page 1 has 35
|
||
// rows × 7 cols (TSR detects one extra spurious column on page 1). The merged
|
||
// table must contain ALL 62 rows (27 + 35) at the shared width of 7 columns.
|
||
func TestMergeTablesAcrossPages_MixedColumnCountsPreservesAllRows(t *testing.T) {
|
||
gridWithTag := func(rows, cols int, tag string) [][]pdf.TSRCell {
|
||
g := make([][]pdf.TSRCell, rows)
|
||
for r := 0; r < rows; r++ {
|
||
g[r] = make([]pdf.TSRCell, cols)
|
||
for c := 0; c < cols; c++ {
|
||
g[r][c] = pdf.TSRCell{
|
||
X0: float64(c) * 100, Y0: float64(r) * 30,
|
||
X1: float64(c)*100 + 100, Y1: float64(r)*30 + 30,
|
||
Text: fmt.Sprintf("%s_r%d_c%d", tag, r, c),
|
||
}
|
||
}
|
||
}
|
||
return g
|
||
}
|
||
pg0 := pdf.TableItem{
|
||
Positions: []pdf.Position{{PageNumbers: []int{0}, Left: 0, Right: 600, Top: 0, Bottom: 810}},
|
||
Scale: 1.0,
|
||
Grid: gridWithTag(27, 6, "p0"),
|
||
}
|
||
pg1 := pdf.TableItem{
|
||
Positions: []pdf.Position{{PageNumbers: []int{1}, Left: 0, Right: 700, Top: 0, Bottom: 1050}},
|
||
Scale: 1.0,
|
||
Grid: gridWithTag(35, 7, "p1"),
|
||
}
|
||
|
||
merged := MergeTablesAcrossPages([]pdf.TableItem{pg0, pg1}, nil)
|
||
if len(merged) != 1 {
|
||
t.Fatalf("expected 1 merged table, got %d", len(merged))
|
||
}
|
||
// All 62 rows (27 + 35) must survive the cross-page merge.
|
||
if len(merged[0].Grid) != 62 {
|
||
t.Fatalf("merged Grid must contain all rows from both pages (want 62), got %d", len(merged[0].Grid))
|
||
}
|
||
// Shared width is the max column count (7) for every row.
|
||
for r, row := range merged[0].Grid {
|
||
if len(row) != 7 {
|
||
t.Errorf("row %d: aligned grid width must be max cols (7), got %d", r, len(row))
|
||
}
|
||
}
|
||
// Anchor rows first, continuation rows appended, both complete.
|
||
if merged[0].Grid[0][0].Text != "p0_r0_c0" {
|
||
t.Errorf("first anchor row lost: %s", merged[0].Grid[0][0].Text)
|
||
}
|
||
if merged[0].Grid[26][5].Text != "p0_r26_c5" {
|
||
t.Errorf("last anchor row lost: %s", merged[0].Grid[26][5].Text)
|
||
}
|
||
if merged[0].Grid[27][0].Text != "p1_r0_c0" {
|
||
t.Errorf("first continuation row lost: %s", merged[0].Grid[27][0].Text)
|
||
}
|
||
if merged[0].Grid[61][6].Text != "p1_r34_c6" {
|
||
t.Errorf("last continuation row lost: %s", merged[0].Grid[61][6].Text)
|
||
}
|
||
}
|
||
|
||
// TestMergeTablesAcrossPages_ThreePageCumulativeShift verifies that with three
|
||
// consecutive pages the per-page grids stack cumulatively: each continuation
|
||
// page sits strictly below the previous page's last row, and the Y shift
|
||
// accumulates (page2 below page1 below page0). Catches a regression where
|
||
// stackGrids resets prevMaxY to the anchor instead of carrying the prior
|
||
// page's shifted bottom forward.
|
||
func TestMergeTablesAcrossPages_ThreePageCumulativeShift(t *testing.T) {
|
||
pageGrid := func(rows [][]string) [][]pdf.TSRCell {
|
||
g := make([][]pdf.TSRCell, len(rows))
|
||
for r, row := range rows {
|
||
g[r] = make([]pdf.TSRCell, len(row))
|
||
for c := range row {
|
||
g[r][c] = pdf.TSRCell{
|
||
X0: float64(c) * 100, Y0: float64(r) * 30,
|
||
X1: float64(c)*100 + 100, Y1: float64(r)*30 + 30,
|
||
Text: row[c],
|
||
}
|
||
}
|
||
}
|
||
return g
|
||
}
|
||
// Three pages, 2 rows × 2 cols each, identical layout.
|
||
pages := []pdf.TableItem{
|
||
{Positions: []pdf.Position{{PageNumbers: []int{0}, Left: 0, Right: 200, Top: 0, Bottom: 60}}, Scale: 1.0, Grid: pageGrid([][]string{{"a", "b"}, {"c", "d"}})},
|
||
{Positions: []pdf.Position{{PageNumbers: []int{1}, Left: 0, Right: 200, Top: 0, Bottom: 60}}, Scale: 1.0, Grid: pageGrid([][]string{{"e", "f"}, {"g", "h"}})},
|
||
{Positions: []pdf.Position{{PageNumbers: []int{2}, Left: 0, Right: 200, Top: 0, Bottom: 60}}, Scale: 1.0, Grid: pageGrid([][]string{{"i", "j"}, {"k", "l"}})},
|
||
}
|
||
|
||
merged := MergeTablesAcrossPages(pages, nil)
|
||
if len(merged) != 1 {
|
||
t.Fatalf("expected 1 merged table, got %d", len(merged))
|
||
}
|
||
// 3 pages × 2 rows → 6 rows, in page order.
|
||
if len(merged[0].Grid) != 6 {
|
||
t.Fatalf("3-page merge must stack all rows (want 6), got %d", len(merged[0].Grid))
|
||
}
|
||
if merged[0].Grid[0][0].Text != "a" || merged[0].Grid[2][0].Text != "e" || merged[0].Grid[4][0].Text != "i" {
|
||
t.Errorf("row order wrong after 3-page stacking: %s / %s / %s",
|
||
merged[0].Grid[0][0].Text, merged[0].Grid[2][0].Text, merged[0].Grid[4][0].Text)
|
||
}
|
||
// Cumulative Y shift: page1 strictly below page0, page2 strictly below
|
||
// page1, and page2 below page1 (monotonic accumulation).
|
||
if merged[0].Grid[2][0].Y0 <= merged[0].Grid[1][0].Y1 {
|
||
t.Errorf("page1 not shifted below page0 (Grid[2][0].Y0=%v <= Grid[1][0].Y1=%v)",
|
||
merged[0].Grid[2][0].Y0, merged[0].Grid[1][0].Y1)
|
||
}
|
||
if merged[0].Grid[4][0].Y0 <= merged[0].Grid[3][0].Y1 {
|
||
t.Errorf("page2 not shifted below page1 (Grid[4][0].Y0=%v <= Grid[3][0].Y1=%v)",
|
||
merged[0].Grid[4][0].Y0, merged[0].Grid[3][0].Y1)
|
||
}
|
||
if merged[0].Grid[4][0].Y0 <= merged[0].Grid[2][0].Y0 {
|
||
t.Errorf("Y shift not cumulative: page2 (Y0=%v) must be below page1 (Y0=%v)",
|
||
merged[0].Grid[4][0].Y0, merged[0].Grid[2][0].Y0)
|
||
}
|
||
}
|
||
|
||
// TestMergeTablesAcrossPages_GridlessAnchorUnchanged verifies that when the
|
||
// anchor has no Grid, MergeTablesAcrossPages skips the rebuild entirely and
|
||
// leaves anchor.Grid empty (nil) so ConstructTable falls back to the Cells
|
||
// path — the same pre-fix behaviour. This locks the no-regression promise the
|
||
// fix relies on for Grid-less tables (and the 7 pre-existing tests that set no
|
||
// Grid). The cross-page merge decision itself is unchanged: continuation
|
||
// Cells and Positions are still appended.
|
||
func TestMergeTablesAcrossPages_GridlessAnchorUnchanged(t *testing.T) {
|
||
cells := func(texts []string) []pdf.TSRCell {
|
||
cs := make([]pdf.TSRCell, len(texts))
|
||
for i, txt := range texts {
|
||
cs[i] = pdf.TSRCell{
|
||
X0: 0, Y0: float64(i) * 30, X1: 100, Y1: float64(i)*30 + 30,
|
||
Text: txt,
|
||
}
|
||
}
|
||
return cs
|
||
}
|
||
// Anchor has Cells but NO Grid; continuation also Grid-less.
|
||
pg0 := pdf.TableItem{
|
||
Positions: []pdf.Position{{PageNumbers: []int{0}, Left: 0, Right: 200, Top: 0, Bottom: 60}},
|
||
Scale: 1.0,
|
||
Cells: cells([]string{"a", "b", "c", "d"}),
|
||
}
|
||
pg1 := pdf.TableItem{
|
||
Positions: []pdf.Position{{PageNumbers: []int{1}, Left: 0, Right: 200, Top: 0, Bottom: 60}},
|
||
Scale: 1.0,
|
||
Cells: cells([]string{"e", "f", "g", "h"}),
|
||
}
|
||
|
||
merged := MergeTablesAcrossPages([]pdf.TableItem{pg0, pg1}, nil)
|
||
if len(merged) != 1 {
|
||
t.Fatalf("expected 1 merged table, got %d", len(merged))
|
||
}
|
||
// Guard: anchor has no Grid → rebuild skipped entirely → Grid stays empty.
|
||
if len(merged[0].Grid) != 0 {
|
||
t.Fatalf("Grid-less anchor must keep Grid empty (rebuild skipped), got %d rows", len(merged[0].Grid))
|
||
}
|
||
// Merge decision unchanged: all continuation Cells still appended.
|
||
have := map[string]bool{}
|
||
for _, c := range merged[0].Cells {
|
||
have[c.Text] = true
|
||
}
|
||
for _, want := range []string{"a", "b", "c", "d", "e", "f", "g", "h"} {
|
||
if !have[want] {
|
||
t.Errorf("merged Cells missing %q (merge decision changed for Grid-less anchor)", want)
|
||
}
|
||
}
|
||
// Positions from both pages present → the cross-page merge did happen.
|
||
pages := map[int]bool{}
|
||
for _, p := range merged[0].Positions {
|
||
for _, pn := range p.PageNumbers {
|
||
pages[pn] = true
|
||
}
|
||
}
|
||
if !pages[0] || !pages[1] {
|
||
t.Errorf("cross-page merge did not combine both pages' positions: %v", pages)
|
||
}
|
||
}
|