Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// Package ocr provides local recognition without build-time OCR dependencies.
|
||||
// Captures, review sessions and native input remain separate owners.
|
||||
package ocr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"math"
|
||||
"sort"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnavailable = errors.New("OCR not available")
|
||||
ErrInvalidImage = errors.New("invalid OCR image")
|
||||
ErrInvalidOutput = errors.New("invalid OCR output")
|
||||
ErrLimit = errors.New("OCR limit exceeded")
|
||||
ErrRecognition = errors.New("OCR recognition failed")
|
||||
)
|
||||
|
||||
const (
|
||||
MaxPixels = 16_000_000
|
||||
MaxTextBytes = 1 << 20
|
||||
maxTSVBytes = 8 << 20
|
||||
maxWords = 100_000
|
||||
maxRows = 200_000
|
||||
)
|
||||
|
||||
// LineID preserves the full engine hierarchy, including line numbers which
|
||||
// restart in each paragraph. Result order is engine order, not screen Y/X order.
|
||||
type LineID struct {
|
||||
Page, Block, Paragraph, Line int
|
||||
}
|
||||
|
||||
// Span is a half-open range of Unicode code points, never UTF-8 bytes.
|
||||
// Combining marks retain their original offsets; text is not normalized.
|
||||
type Span struct {
|
||||
Start, End int
|
||||
}
|
||||
|
||||
type Word struct {
|
||||
Text string
|
||||
Confidence float64
|
||||
Bounds image.Rectangle
|
||||
Span
|
||||
}
|
||||
|
||||
type Line struct {
|
||||
ID LineID
|
||||
Span
|
||||
WordStart, WordEnd int
|
||||
}
|
||||
|
||||
// Result owns plain text and word rectangles in recognition-image pixels.
|
||||
// An empty document is a successful recognition with no text.
|
||||
type Result struct {
|
||||
Text string
|
||||
Width, Height int
|
||||
Words []Word
|
||||
Lines []Line
|
||||
}
|
||||
|
||||
// WordAt deliberately rejects separators and end-of-document positions.
|
||||
// A later activation owner must validate source freshness before clicking.
|
||||
func (result Result) WordAt(offset int) (Word, bool) {
|
||||
index := sort.Search(len(result.Words), func(index int) bool {
|
||||
return result.Words[index].End > offset
|
||||
})
|
||||
if index == len(result.Words) || offset < result.Words[index].Start {
|
||||
return Word{}, false
|
||||
}
|
||||
word := result.Words[index]
|
||||
runeIndex := 0
|
||||
for _, char := range word.Text {
|
||||
if runeIndex == offset-word.Start {
|
||||
return word, !unicode.IsSpace(char)
|
||||
}
|
||||
runeIndex++
|
||||
}
|
||||
return Word{}, false
|
||||
}
|
||||
|
||||
// ByteOffset explicitly bridges the reader's rune offsets to Go string slices.
|
||||
// The offset at the end of the document is valid.
|
||||
func (result Result) ByteOffset(offset int) (int, bool) {
|
||||
if offset < 0 {
|
||||
return 0, false
|
||||
}
|
||||
runeIndex := 0
|
||||
for byteIndex := range result.Text {
|
||||
if runeIndex == offset {
|
||||
return byteIndex, true
|
||||
}
|
||||
runeIndex++
|
||||
}
|
||||
return len(result.Text), runeIndex == offset
|
||||
}
|
||||
|
||||
// Transform maps the content rectangle within the processed image to the
|
||||
// clipped capture rectangle on screen. ImageContent accounts for padding;
|
||||
// independent ratios account for rounded scaling in each axis. A clipboard
|
||||
// image has HasScreenMapping=false and cannot supply screen coordinates.
|
||||
// This mapping says nothing about source identity, freshness or monitor gaps.
|
||||
type Transform struct {
|
||||
ImageContent image.Rectangle
|
||||
ScreenBounds image.Rectangle
|
||||
HasScreenMapping bool
|
||||
}
|
||||
|
||||
// ScreenRectangle rounds outward and clips padding to the actual capture.
|
||||
// Invalid, empty and entirely padded rectangles have no screen mapping.
|
||||
func (transform Transform) ScreenRectangle(bounds image.Rectangle) (image.Rectangle, bool) {
|
||||
content, screen := transform.ImageContent, transform.ScreenBounds
|
||||
if !transform.HasScreenMapping || !valid_dimensions(content.Dx(), content.Dy()) ||
|
||||
content.Min.X < 0 || content.Min.Y < 0 || content.Max.X > MaxPixels || content.Max.Y > MaxPixels ||
|
||||
!valid_screen_rectangle(screen) {
|
||||
return image.Rectangle{}, false
|
||||
}
|
||||
bounds = bounds.Intersect(content)
|
||||
if bounds.Empty() {
|
||||
return image.Rectangle{}, false
|
||||
}
|
||||
mapX := func(x int) float64 {
|
||||
return float64(screen.Min.X) + float64(x-content.Min.X)*float64(screen.Dx())/float64(content.Dx())
|
||||
}
|
||||
mapY := func(y int) float64 {
|
||||
return float64(screen.Min.Y) + float64(y-content.Min.Y)*float64(screen.Dy())/float64(content.Dy())
|
||||
}
|
||||
mapped := image.Rect(int(math.Floor(mapX(bounds.Min.X))), int(math.Floor(mapY(bounds.Min.Y))),
|
||||
int(math.Ceil(mapX(bounds.Max.X))), int(math.Ceil(mapY(bounds.Max.Y))))
|
||||
mapped = mapped.Intersect(screen)
|
||||
return mapped, !mapped.Empty()
|
||||
}
|
||||
|
||||
func valid_screen_rectangle(bounds image.Rectangle) bool {
|
||||
return bounds.Min.X > math.MinInt32 && bounds.Min.Y > math.MinInt32 &&
|
||||
bounds.Max.X <= math.MaxInt32 && bounds.Max.Y <= math.MaxInt32 &&
|
||||
bounds.Max.X > bounds.Min.X && bounds.Max.Y > bounds.Min.Y &&
|
||||
int64(bounds.Max.X)-int64(bounds.Min.X) <= MaxPixels &&
|
||||
int64(bounds.Max.Y)-int64(bounds.Min.Y) <= MaxPixels
|
||||
}
|
||||
|
||||
func valid_dimensions(width, height int) bool {
|
||||
return width > 0 && height > 0 && height <= MaxPixels && width <= MaxPixels/height
|
||||
}
|
||||
|
||||
func valid_word_bounds(bounds image.Rectangle, width, height int) bool {
|
||||
return bounds.Min.X >= 0 && bounds.Min.Y >= 0 && !bounds.Empty() &&
|
||||
bounds.Max.X <= width && bounds.Max.Y <= height
|
||||
}
|
||||
|
||||
func rune_length(text string) int { return utf8.RuneCountInString(text) }
|
||||
Reference in new issue
Block a user