mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-11-13 23:01:11 +08:00
205 lines
7.4 KiB
Go
205 lines
7.4 KiB
Go
package parser
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
|
|
"ragflow/internal/entity"
|
|
"ragflow/internal/entity/models"
|
|
"ragflow/internal/utility"
|
|
)
|
|
|
|
// isStructuralOCRTextLayout reports whether an item already carries a
|
|
// structural block kind produced by markdown normalization. Those values are
|
|
// finer than an OCR block label and must win over it.
|
|
func isStructuralOCRTextLayout(fields map[string]any) bool {
|
|
layout, _ := fields["layout"].(string)
|
|
switch layout {
|
|
case "heading", "paragraph", "list", "code", "quote":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// OCRPDFResult projects the page blocks of an OCR provider onto the PDF's page
|
|
// geometry and emits the structured items consumed by the existing PDF chunk
|
|
// pipeline. provider names the OCR service in errors and warnings.
|
|
func OCRPDFResult(ctx context.Context, provider, filename string, data []byte, response *models.OCRFileResponse) ParseResult {
|
|
if err := ctx.Err(); err != nil {
|
|
return ParseResult{Err: err}
|
|
}
|
|
if response == nil || len(response.Pages) == 0 {
|
|
return ParseResult{Err: fmt.Errorf("parser: %s returned no page blocks", provider)}
|
|
}
|
|
sizes, pageCount, geometryErr := paddleOCRPageSizes(ctx, data, response.Pages)
|
|
if err := ctx.Err(); err != nil {
|
|
return ParseResult{Err: err}
|
|
}
|
|
result := ParseResult{Warnings: append([]string(nil), response.Warnings...)}
|
|
hasPageCount := pageCount > 0
|
|
if geometryErr != nil {
|
|
result.Warnings = append(result.Warnings, provider+" PDF geometry unavailable: "+geometryErr.Error())
|
|
}
|
|
markdown, err := NewMarkdownParser(GoMarkdown)
|
|
if err != nil {
|
|
return ParseResult{Err: err}
|
|
}
|
|
markdown.FetchRemoteImages = false
|
|
for _, page := range response.Pages {
|
|
if !hasPageCount && page.Index+1 > pageCount {
|
|
pageCount = page.Index + 1
|
|
}
|
|
size := sizes[page.Index]
|
|
if paddleOCRPositive(page.Width) && paddleOCRPositive(page.Height) && paddleOCRPositive(size[0]) && paddleOCRPositive(size[1]) && !paddleOCRAspectRatioMatches(page, size) {
|
|
result.Warnings = append(result.Warnings, fmt.Sprintf("%s page %d: OCR dimensions %gx%g do not match PDF aspect ratio %gx%g; coordinates omitted", provider, page.Index+1, page.Width, page.Height, size[0], size[1]))
|
|
size = [2]float64{}
|
|
}
|
|
for _, block := range page.Blocks {
|
|
if err := ctx.Err(); err != nil {
|
|
return ParseResult{Err: err}
|
|
}
|
|
label := strings.ToLower(strings.TrimSpace(block.Label))
|
|
if models.IsOCRMediaLabel(label) || models.IsOCRImageOnlyText(block.Text) {
|
|
text := strings.TrimSpace(utility.StripMarkdownImages(block.Text))
|
|
position := paddleOCRBlockPosition(page, block.BBox, size)
|
|
// A block with neither body text nor a crop region has nothing
|
|
// media can act on: a crop preview needs positive-area geometry,
|
|
// and a synthetic text body would index as retrievable text.
|
|
if text == "" && len(position) == 0 {
|
|
continue
|
|
}
|
|
result.JSON = append(result.JSON, paddleOCRMediaItem(page, text, position))
|
|
continue
|
|
}
|
|
content := strings.TrimSpace(utility.StripMarkdownImages(block.Text))
|
|
if content == "" {
|
|
continue
|
|
}
|
|
parsed := markdown.ParseWithResult(ctx, filename, []byte(content+"\n\n"))
|
|
if parsed.Err != nil {
|
|
return parsed
|
|
}
|
|
result.Warnings = append(result.Warnings, parsed.Warnings...)
|
|
position := paddleOCRBlockPosition(page, block.BBox, size)
|
|
isTitle := label == "doc_title" || label == "paragraph_title"
|
|
if label == "figure_title" || label == "table_title" {
|
|
label = "caption"
|
|
}
|
|
for _, item := range parsed.JSON {
|
|
_, isTextItem := item.(TextItem)
|
|
if text, ok := item.(TextItem); ok {
|
|
if strings.TrimSpace(text.Text) == "" {
|
|
continue
|
|
}
|
|
layout, _ := text.Extra["layout"].(string)
|
|
if isTitle && layout != "heading" && layout != "list" && layout != "code" {
|
|
prefix := "## "
|
|
if label == "doc_title" {
|
|
prefix = "# "
|
|
}
|
|
text.Text = prefix + text.Text
|
|
if text.Extra == nil {
|
|
text.Extra = map[string]any{}
|
|
}
|
|
text.Extra["layout"] = "heading"
|
|
item = text
|
|
}
|
|
}
|
|
fields := item.ToMap()
|
|
// Page identity is retained even when the block has no usable geometry.
|
|
fields["page_number"] = page.Index + 1
|
|
fields["_preserve_order"] = true
|
|
// Markdown normalization already asserted a structural layout
|
|
// (heading/list/code). The OCR block label is coarser and must
|
|
// not overwrite it; only fill an absent layout.
|
|
if isTitle && isTextItem && !isStructuralOCRTextLayout(fields) {
|
|
// The asserted-title marker the Title chunker reads without
|
|
// the notTitle heuristic guard. PDF model labels that merely
|
|
// look title-like stay on "title".
|
|
fields["layout"] = "heading"
|
|
} else if label != "" {
|
|
if existing, _ := fields["layout"].(string); existing == "" {
|
|
fields["layout"] = label
|
|
}
|
|
}
|
|
if len(position) > 0 {
|
|
matrix := [][]any{position}
|
|
fields["positions"], fields["_pdf_positions"] = matrix, matrix
|
|
}
|
|
if table, ok := fields["table"].(*entity.TableData); ok && table != nil {
|
|
table.Page = page.Index + 1
|
|
}
|
|
result.JSON = append(result.JSON, DocItemFromMap(fields))
|
|
}
|
|
}
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return ParseResult{Err: err}
|
|
}
|
|
if len(result.JSON) == 0 {
|
|
return ParseResult{Err: fmt.Errorf("parser: %s returned empty text", provider)}
|
|
}
|
|
result.File = pdfFileMeta(filename, pageCount)
|
|
return result
|
|
}
|
|
|
|
// paddleOCRMediaItem keeps a figure-like block as a media item. The text stays
|
|
// empty when the block carries no body text: the region is preserved for the
|
|
// vision pass and the on-demand crop, while a synthetic placeholder would
|
|
// index as retrievable body text (#21218). The chunker drops a text-less media
|
|
// item that never became retrievable, matching the DeepDoc figure path.
|
|
func paddleOCRMediaItem(page models.OCRPage, text string, position []any) DocItem {
|
|
fields := map[string]any{
|
|
"doc_type_kwd": "image",
|
|
"layout": "figure",
|
|
"page_number": page.Index + 1,
|
|
"_preserve_order": true,
|
|
}
|
|
if text != "" {
|
|
fields["text"] = text
|
|
}
|
|
if len(position) > 0 {
|
|
matrix := [][]any{position}
|
|
fields["positions"], fields["_pdf_positions"] = matrix, matrix
|
|
}
|
|
return DocItemFromMap(fields)
|
|
}
|
|
|
|
func paddleOCRBlockPosition(page models.OCRPage, bbox []float64, size [2]float64) []any {
|
|
if page.Index < 0 || !paddleOCRPositive(page.Width) || !paddleOCRPositive(page.Height) || !paddleOCRPositive(size[0]) || !paddleOCRPositive(size[1]) {
|
|
return nil
|
|
}
|
|
if !paddleOCRAspectRatioMatches(page, size) || len(bbox) != 4 {
|
|
return nil
|
|
}
|
|
for _, v := range bbox {
|
|
if math.IsNaN(v) || math.IsInf(v, 0) {
|
|
return nil
|
|
}
|
|
}
|
|
left, top, right, bottom := bbox[0], bbox[1], bbox[2], bbox[3]
|
|
if right <= left || bottom <= top {
|
|
return nil
|
|
}
|
|
left = math.Max(0, math.Min(size[0], left*size[0]/page.Width))
|
|
right = math.Max(0, math.Min(size[0], right*size[0]/page.Width))
|
|
top = math.Max(0, math.Min(size[1], top*size[1]/page.Height))
|
|
bottom = math.Max(0, math.Min(size[1], bottom*size[1]/page.Height))
|
|
if right <= left || bottom <= top {
|
|
return nil
|
|
}
|
|
return []any{page.Index + 1, left, right, top, bottom}
|
|
}
|
|
|
|
// Allow 2% variation for renderer pixel rounding, but reject anisotropic scaling.
|
|
func paddleOCRAspectRatioMatches(page models.OCRPage, size [2]float64) bool {
|
|
xScale, yScale := size[0]/page.Width, size[1]/page.Height
|
|
return math.Max(xScale, yScale) <= 1.02*math.Min(xScale, yScale)
|
|
}
|
|
|
|
func paddleOCRPositive(value float64) bool {
|
|
return value > 0 && !math.IsInf(value, 0) && !math.IsNaN(value)
|
|
}
|