mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-18 21:57:27 +08:00
Fix centered PDF title reading order (#17009)
This commit is contained in:
@@ -357,10 +357,9 @@ func mergeTwoBoxes(prev, curr pdf.TextBox) pdf.TextBox {
|
||||
}
|
||||
|
||||
// processPageBoxes vertically merges the boxes of a single page. Boxes are
|
||||
// bucketed by column (ColID) first so the merge never crosses columns; within
|
||||
// a column the merge runs top→bottom, and columns are emitted in ascending
|
||||
// ColID order (leftmost first). This preserves the column-major reading order
|
||||
// established by FinalReadingOrderMerge (page → ColID → top → x0).
|
||||
// bucketed by column first so merges never cross columns. Titles that precede
|
||||
// all non-title content and occupy their own column are moved ahead of the
|
||||
// column groups.
|
||||
func processPageBoxes(boxes []pdf.TextBox, mh, mw float64, isEnglish bool) []pdf.TextBox {
|
||||
if len(boxes) == 0 {
|
||||
return boxes
|
||||
@@ -384,7 +383,7 @@ func processPageBoxes(boxes []pdf.TextBox, mh, mw float64, isEnglish bool) []pdf
|
||||
})
|
||||
out = append(out, mergeColumnBoxes(bxs, mh, mw, isEnglish)...)
|
||||
}
|
||||
return out
|
||||
return moveLeadingTitlesFirst(out)
|
||||
}
|
||||
|
||||
// groupBoxesByCol groups boxes by column id and returns the groups plus the
|
||||
@@ -444,6 +443,35 @@ func mergeColumnBoxes(sortedBoxes []pdf.TextBox, mh, mw float64, isEnglish bool)
|
||||
return out
|
||||
}
|
||||
|
||||
func moveLeadingTitlesFirst(boxes []pdf.TextBox) []pdf.TextBox {
|
||||
firstNonTitleTop := math.Inf(1)
|
||||
colsWithNonTitle := make(map[int]struct{})
|
||||
for _, box := range boxes {
|
||||
if box.LayoutType != pdf.LayoutTypeTitle {
|
||||
firstNonTitleTop = math.Min(firstNonTitleTop, box.Top)
|
||||
colsWithNonTitle[box.ColID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
titles := make([]pdf.TextBox, 0)
|
||||
rest := make([]pdf.TextBox, 0, len(boxes))
|
||||
for _, box := range boxes {
|
||||
_, sharesColumnWithContent := colsWithNonTitle[box.ColID]
|
||||
if box.LayoutType == pdf.LayoutTypeTitle && !sharesColumnWithContent && box.Bottom <= firstNonTitleTop {
|
||||
titles = append(titles, box)
|
||||
continue
|
||||
}
|
||||
rest = append(rest, box)
|
||||
}
|
||||
sort.SliceStable(titles, func(i, j int) bool {
|
||||
if titles[i].Top != titles[j].Top {
|
||||
return titles[i].Top < titles[j].Top
|
||||
}
|
||||
return titles[i].X0 < titles[j].X0
|
||||
})
|
||||
return append(titles, rest...)
|
||||
}
|
||||
|
||||
// ---- rune-based text helpers (CJK-safe) ----
|
||||
|
||||
func lastRune(s string) rune {
|
||||
|
||||
@@ -516,6 +516,27 @@ func TestNaiveVerticalMergeNonMerge(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaiveVerticalMerge_CenteredTitlePreservesPageOrder(t *testing.T) {
|
||||
boxes := []pdf.TextBox{
|
||||
{PageNumber: 0, X0: 250, X1: 450, Top: 50, Bottom: 70, Text: "Document Title", LayoutNo: "title", LayoutType: pdf.LayoutTypeTitle},
|
||||
{PageNumber: 0, X0: 50, X1: 650, Top: 100, Bottom: 112, Text: "First paragraph.", LayoutNo: "body"},
|
||||
{PageNumber: 0, X0: 50, X1: 650, Top: 200, Bottom: 212, Text: "Second paragraph.", LayoutNo: "body"},
|
||||
}
|
||||
|
||||
boxes = AssignColumn(boxes)
|
||||
result := NaiveVerticalMerge(boxes, map[int]float64{0: 12}, map[int]float64{0: 5}, map[int]bool{0: true})
|
||||
|
||||
want := []string{"Document Title", "First paragraph.", "Second paragraph."}
|
||||
if len(result) != len(want) {
|
||||
t.Fatalf("expected %d boxes, got %d", len(want), len(result))
|
||||
}
|
||||
for i, text := range want {
|
||||
if result[i].Text != text {
|
||||
t.Errorf("position %d: want %q, got %q", i, text, result[i].Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNaiveVerticalMerge_MultiColumnOrder guards against the multi-column
|
||||
// reading-order regression: after AssignColumn assigns ColID, the final
|
||||
// reading order must be column-major (all of column 0, then all of column 1),
|
||||
@@ -582,6 +603,66 @@ func TestNaiveVerticalMerge_MultiColumnOrder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaiveVerticalMerge_LeadingTitlePreservesMultiColumnOrder(t *testing.T) {
|
||||
boxes := []pdf.TextBox{
|
||||
{PageNumber: 0, ColID: 2, X0: 250, X1: 450, Top: 50, Bottom: 70, Text: "Document Title", LayoutNo: "title", LayoutType: pdf.LayoutTypeTitle},
|
||||
{PageNumber: 0, ColID: 0, X0: 50, X1: 250, Top: 100, Bottom: 112, Text: "L0-a", LayoutNo: "left"},
|
||||
{PageNumber: 0, ColID: 0, X0: 50, X1: 250, Top: 300, Bottom: 312, Text: "L0-b", LayoutNo: "left"},
|
||||
{PageNumber: 0, ColID: 1, X0: 400, X1: 600, Top: 150, Bottom: 162, Text: "R0-a", LayoutNo: "right"},
|
||||
{PageNumber: 0, ColID: 1, X0: 400, X1: 600, Top: 250, Bottom: 262, Text: "R0-b", LayoutNo: "right"},
|
||||
}
|
||||
|
||||
result := NaiveVerticalMerge(boxes, map[int]float64{0: 12}, map[int]float64{0: 5}, nil)
|
||||
want := []string{"Document Title", "L0-a", "L0-b", "R0-a", "R0-b"}
|
||||
if len(result) != len(want) {
|
||||
t.Fatalf("expected %d boxes, got %d", len(want), len(result))
|
||||
}
|
||||
for i, text := range want {
|
||||
if result[i].Text != text {
|
||||
t.Errorf("position %d: want %q, got %q", i, text, result[i].Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaiveVerticalMerge_DoesNotPromoteColumnTitlePastEarlierBody(t *testing.T) {
|
||||
boxes := []pdf.TextBox{
|
||||
{PageNumber: 0, ColID: 0, X0: 50, X1: 250, Top: 100, Bottom: 112, Text: "Left body", LayoutNo: "left"},
|
||||
{PageNumber: 0, ColID: 1, X0: 400, X1: 600, Top: 150, Bottom: 162, Text: "Right heading", LayoutNo: "right-title", LayoutType: pdf.LayoutTypeTitle},
|
||||
{PageNumber: 0, ColID: 1, X0: 400, X1: 600, Top: 200, Bottom: 212, Text: "Right body", LayoutNo: "right"},
|
||||
}
|
||||
|
||||
result := NaiveVerticalMerge(boxes, map[int]float64{0: 12}, map[int]float64{0: 5}, nil)
|
||||
want := []string{"Left body", "Right heading", "Right body"}
|
||||
if len(result) != len(want) {
|
||||
t.Fatalf("expected %d boxes, got %d", len(want), len(result))
|
||||
}
|
||||
for i, text := range want {
|
||||
if result[i].Text != text {
|
||||
t.Errorf("position %d: want %q, got %q", i, text, result[i].Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaiveVerticalMerge_DoesNotPromoteTitlesFromBodyColumns(t *testing.T) {
|
||||
boxes := []pdf.TextBox{
|
||||
{PageNumber: 0, ColID: 0, X0: 50, X1: 250, Top: 50, Bottom: 62, Text: "Left heading", LayoutNo: "left-title", LayoutType: pdf.LayoutTypeTitle},
|
||||
{PageNumber: 0, ColID: 0, X0: 50, X1: 250, Top: 100, Bottom: 112, Text: "Left body", LayoutNo: "left"},
|
||||
{PageNumber: 0, ColID: 1, X0: 400, X1: 600, Top: 60, Bottom: 72, Text: "Right heading", LayoutNo: "right-title", LayoutType: pdf.LayoutTypeTitle},
|
||||
{PageNumber: 0, ColID: 1, X0: 400, X1: 600, Top: 120, Bottom: 132, Text: "Right body", LayoutNo: "right"},
|
||||
}
|
||||
|
||||
result := NaiveVerticalMerge(boxes, map[int]float64{0: 12}, map[int]float64{0: 5}, nil)
|
||||
want := []string{"Left heading", "Left body", "Right heading", "Right body"}
|
||||
if len(result) != len(want) {
|
||||
t.Fatalf("expected %d boxes, got %d", len(want), len(result))
|
||||
}
|
||||
for i, text := range want {
|
||||
if result[i].Text != text {
|
||||
t.Errorf("position %d: want %q, got %q", i, text, result[i].Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinalReadingOrderMerge_ColumnMajor(t *testing.T) {
|
||||
// Same interleaved scenario as the pipeline test, but at the
|
||||
// FinalReadingOrderMerge level: column must dominate vertical position.
|
||||
|
||||
@@ -456,9 +456,8 @@ func (p *Parser) buildLayout(ctx context.Context,
|
||||
boxes = lyt.TextMerge(boxes, medianHeights)
|
||||
result.Metrics.BoxesTextMerge = len(boxes)
|
||||
|
||||
// FinalReadingOrderMerge sorts page → column (ColID) → top → x0, which
|
||||
// is the correct multi-column reading order. NaiveVerticalMerge preserves
|
||||
// this column-major order (it buckets by ColID before merging).
|
||||
// Preserve column-major content order while NaiveVerticalMerge promotes a
|
||||
// page-leading title isolated in its own column.
|
||||
boxes = lyt.FinalReadingOrderMerge(boxes)
|
||||
|
||||
boxes = lyt.NaiveVerticalMerge(boxes, medianHeights, medianWidths, pageEnglish)
|
||||
|
||||
Reference in New Issue
Block a user