feat: Add .env.example configuration file and integrate Redis service into Docker Compose for improved task management. Update desktop application to support document indexing and extraction workflows, enhancing user experience with a new extraction wizard and stepper components. Revise README and documentation for clarity on setup and usage.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
package ocr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
|
||||
"github.com/gen2brain/go-fitz"
|
||||
"github.com/otiai10/gosseract/v2"
|
||||
)
|
||||
|
||||
type Box struct {
|
||||
Text string
|
||||
X int
|
||||
Y int
|
||||
W int
|
||||
H int
|
||||
}
|
||||
|
||||
// RenderPage rasterizes the first page of a PDF.
|
||||
func RenderPage(pdfData []byte, dpi int) (image.Image, int, int, error) {
|
||||
doc, err := fitz.NewFromMemory(pdfData)
|
||||
if err != nil {
|
||||
return nil, 0, 0, fmt.Errorf("open pdf: %w", err)
|
||||
}
|
||||
defer doc.Close()
|
||||
|
||||
if doc.NumPage() == 0 {
|
||||
return nil, 0, 0, fmt.Errorf("pdf has no pages")
|
||||
}
|
||||
|
||||
img, err := doc.ImageDPI(0, float64(dpi))
|
||||
if err != nil {
|
||||
return nil, 0, 0, fmt.Errorf("render page: %w", err)
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
return img, bounds.Dx(), bounds.Dy(), nil
|
||||
}
|
||||
|
||||
// OCRImage runs Tesseract word-level OCR on a rendered page image.
|
||||
func (e *Engine) OCRImage(img image.Image) ([]Box, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
return nil, fmt.Errorf("encode png: %w", err)
|
||||
}
|
||||
|
||||
client := e.Acquire()
|
||||
defer e.Release(client)
|
||||
|
||||
if err := client.SetImageFromBytes(buf.Bytes()); err != nil {
|
||||
return nil, fmt.Errorf("tesseract image: %w", err)
|
||||
}
|
||||
|
||||
boxes, err := client.GetBoundingBoxes(gosseract.RIL_WORD)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tesseract ocr: %w", err)
|
||||
}
|
||||
|
||||
out := make([]Box, 0, len(boxes))
|
||||
for _, b := range boxes {
|
||||
text := trim(b.Word)
|
||||
if text == "" || b.Confidence <= 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, Box{
|
||||
Text: text,
|
||||
X: b.Box.Min.X,
|
||||
Y: b.Box.Min.Y,
|
||||
W: b.Box.Dx(),
|
||||
H: b.Box.Dy(),
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ExtractBoxes renders the first PDF page and runs OCR with a pooled Tesseract client.
|
||||
func (e *Engine) ExtractBoxes(pdfData []byte, dpi int) ([]Box, int, int, error) {
|
||||
img, pageW, pageH, err := RenderPage(pdfData, dpi)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
boxes, err := e.OCRImage(img)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
return boxes, pageW, pageH, nil
|
||||
}
|
||||
|
||||
func trim(s string) string {
|
||||
start, end := 0, len(s)
|
||||
for start < end && (s[start] == ' ' || s[start] == '\t' || s[start] == '\n') {
|
||||
start++
|
||||
}
|
||||
for end > start && (s[end-1] == ' ' || s[end-1] == '\t' || s[end-1] == '\n') {
|
||||
end--
|
||||
}
|
||||
return s[start:end]
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package ocr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/otiai10/gosseract/v2"
|
||||
)
|
||||
|
||||
// Engine holds a pool of reusable Tesseract clients.
|
||||
type Engine struct {
|
||||
pool chan *gosseract.Client
|
||||
clients []*gosseract.Client
|
||||
lang string
|
||||
}
|
||||
|
||||
// NewEngine warms up poolSize Tesseract clients for the given language.
|
||||
func NewEngine(lang string, poolSize int) (*Engine, error) {
|
||||
if poolSize < 1 {
|
||||
poolSize = 1
|
||||
}
|
||||
|
||||
e := &Engine{
|
||||
pool: make(chan *gosseract.Client, poolSize),
|
||||
clients: make([]*gosseract.Client, 0, poolSize),
|
||||
lang: lang,
|
||||
}
|
||||
|
||||
for i := 0; i < poolSize; i++ {
|
||||
client, err := newPooledClient(lang)
|
||||
if err != nil {
|
||||
e.Close()
|
||||
return nil, fmt.Errorf("tesseract client %d: %w", i, err)
|
||||
}
|
||||
e.clients = append(e.clients, client)
|
||||
e.pool <- client
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func newPooledClient(lang string) (*gosseract.Client, error) {
|
||||
client := gosseract.NewClient()
|
||||
if err := client.SetLanguage(lang); err != nil {
|
||||
if err2 := client.SetLanguage("eng"); err2 != nil {
|
||||
_ = client.Close()
|
||||
return nil, fmt.Errorf("tesseract lang: %w", err)
|
||||
}
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// Acquire returns a Tesseract client from the pool.
|
||||
func (e *Engine) Acquire() *gosseract.Client {
|
||||
return <-e.pool
|
||||
}
|
||||
|
||||
// Release returns a client to the pool.
|
||||
func (e *Engine) Release(client *gosseract.Client) {
|
||||
e.pool <- client
|
||||
}
|
||||
|
||||
// Close shuts down all pooled clients.
|
||||
func (e *Engine) Close() error {
|
||||
for _, client := range e.clients {
|
||||
_ = client.Close()
|
||||
}
|
||||
e.clients = nil
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user