Reset master to testing content
This commit is contained in:
1 parent
0f2587b719
commit
42afa2a56c
1483 files changed
+145308
-938069
No files matched your search
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package ocr
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const tsvHeader = "level\tpage_num\tblock_num\tpar_num\tline_num\tword_num\tleft\ttop\twidth\theight\tconf\ttext"
|
||||
|
||||
// ParseTSV assembles text and coordinates from a single Tesseract TSV stream.
|
||||
// Malformed output is rejected atomically, with no recognized text in errors.
|
||||
// The supplied dimensions must describe the actual recognition image.
|
||||
func ParseTSV(input io.Reader, width, height int) (Result, error) {
|
||||
if !valid_dimensions(width, height) {
|
||||
return Result{}, ErrInvalidImage
|
||||
}
|
||||
limited := &io.LimitedReader{R: input, N: maxTSVBytes + 1}
|
||||
scanner := bufio.NewScanner(limited)
|
||||
scanner.Buffer(make([]byte, 4096), 64<<10)
|
||||
if !scanner.Scan() || scanner.Text() != tsvHeader {
|
||||
if errors.Is(scanner.Err(), bufio.ErrTooLong) {
|
||||
return Result{}, ErrLimit
|
||||
}
|
||||
return Result{}, fmt.Errorf("%w: TSV header", ErrInvalidOutput)
|
||||
}
|
||||
result := Result{Width: width, Height: height}
|
||||
var text strings.Builder
|
||||
closedLines := make(map[LineID]bool)
|
||||
offset, rows := 0, 0
|
||||
for scanner.Scan() {
|
||||
rows++
|
||||
if rows > maxRows || limited.N <= 0 {
|
||||
return Result{}, ErrLimit
|
||||
}
|
||||
fields := strings.SplitN(scanner.Text(), "\t", 12)
|
||||
if len(fields) != 12 {
|
||||
return Result{}, invalid_row(rows)
|
||||
}
|
||||
var numbers [10]int
|
||||
for index := range numbers {
|
||||
number, err := strconv.ParseInt(fields[index], 10, 32)
|
||||
if err != nil || number < 0 {
|
||||
return Result{}, invalid_row(rows)
|
||||
}
|
||||
numbers[index] = int(number)
|
||||
}
|
||||
level := numbers[0]
|
||||
if level < 1 || level > 5 || numbers[1] < 1 {
|
||||
return Result{}, invalid_row(rows)
|
||||
}
|
||||
for index := 2; index <= 5; index++ {
|
||||
if (index <= level && numbers[index] < 1) || (index > level && numbers[index] != 0) {
|
||||
return Result{}, invalid_row(rows)
|
||||
}
|
||||
}
|
||||
confidence, err := strconv.ParseFloat(fields[10], 64)
|
||||
if err != nil || math.IsNaN(confidence) || math.IsInf(confidence, 0) || confidence < -1 || confidence > 100 {
|
||||
return Result{}, invalid_row(rows)
|
||||
}
|
||||
// Subtraction avoids overflowing 32-bit additions on ARM builds.
|
||||
left, top, boxWidth, boxHeight := numbers[6], numbers[7], numbers[8], numbers[9]
|
||||
if left > width || top > height || boxWidth > width-left || boxHeight > height-top {
|
||||
return Result{}, invalid_row(rows)
|
||||
}
|
||||
wordText := fields[11]
|
||||
if !utf8.ValidString(wordText) || strings.ContainsFunc(wordText, unicode.IsControl) {
|
||||
return Result{}, invalid_row(rows)
|
||||
}
|
||||
if level != 5 {
|
||||
if wordText != "" || confidence != -1 {
|
||||
return Result{}, invalid_row(rows)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(wordText) == "" {
|
||||
continue
|
||||
}
|
||||
bounds := image.Rect(left, top, left+boxWidth, top+boxHeight)
|
||||
if !valid_word_bounds(bounds, width, height) || strings.TrimSpace(wordText) != wordText {
|
||||
return Result{}, invalid_row(rows)
|
||||
}
|
||||
separatorBytes := 0
|
||||
if len(result.Words) != 0 {
|
||||
separatorBytes = 1
|
||||
}
|
||||
if len(result.Words) >= maxWords || text.Len()+len(wordText)+separatorBytes > MaxTextBytes {
|
||||
return Result{}, ErrLimit
|
||||
}
|
||||
id := LineID{numbers[1], numbers[2], numbers[3], numbers[4]}
|
||||
if len(result.Lines) == 0 || result.Lines[len(result.Lines)-1].ID != id {
|
||||
if closedLines[id] {
|
||||
return Result{}, invalid_row(rows)
|
||||
}
|
||||
if len(result.Lines) != 0 {
|
||||
closedLines[result.Lines[len(result.Lines)-1].ID] = true
|
||||
text.WriteByte('\n')
|
||||
offset++
|
||||
}
|
||||
result.Lines = append(result.Lines, Line{ID: id, Span: Span{Start: offset}, WordStart: len(result.Words)})
|
||||
} else {
|
||||
text.WriteByte(' ')
|
||||
offset++
|
||||
}
|
||||
start := offset
|
||||
text.WriteString(wordText)
|
||||
offset += rune_length(wordText)
|
||||
result.Words = append(result.Words, Word{wordText, confidence, bounds, Span{start, offset}})
|
||||
line := &result.Lines[len(result.Lines)-1]
|
||||
line.End, line.WordEnd = offset, len(result.Words)
|
||||
}
|
||||
if limited.N <= 0 {
|
||||
return Result{}, ErrLimit
|
||||
}
|
||||
if scanner.Err() != nil {
|
||||
if errors.Is(scanner.Err(), bufio.ErrTooLong) {
|
||||
return Result{}, ErrLimit
|
||||
}
|
||||
return Result{}, fmt.Errorf("%w: reading TSV", ErrInvalidOutput)
|
||||
}
|
||||
result.Text = text.String()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func invalid_row(row int) error {
|
||||
return fmt.Errorf("%w: TSV row %d", ErrInvalidOutput, row)
|
||||
}
|
||||
Reference in new issue
Block a user