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,147 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package atspi
|
||||
|
||||
/*
|
||||
#include "adapter_native.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CaptureOCRGeometry runs only a narrow target/ancestor query on the GLib
|
||||
// owner. Call it from a worker, not the event loop. A zone remains provider
|
||||
// coordinates and is validated/clipped to the fresh object's bounds.
|
||||
func (adapter *Adapter) CaptureOCRGeometry(ctx context.Context, object ObjectID, zone *Rectangle) (OCRGeometry, error) {
|
||||
if !object.Valid() {
|
||||
return OCRGeometry{}, ErrObjectNotFound
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
|
||||
defer cancel()
|
||||
var ownedZone *Rectangle
|
||||
if zone != nil {
|
||||
copy := *zone
|
||||
ownedZone = ©
|
||||
}
|
||||
result, err := adapter.documentOperation(ctx, treeCaptureRequest{ocrGeometry: true, reviewFocus: object, ocrZone: ownedZone})
|
||||
return result.ocr, err
|
||||
}
|
||||
|
||||
func (adapter *Adapter) nativeOCRGeometry(ctx context.Context, object ObjectID, zone *Rectangle) (OCRGeometry, error) {
|
||||
var source *C.AtspiAccessible
|
||||
for _, item := range adapter.nativeFocus {
|
||||
if item.id == object {
|
||||
source = item.source
|
||||
break
|
||||
}
|
||||
}
|
||||
if source == nil {
|
||||
source = adapter.nativeReview[object]
|
||||
}
|
||||
if source == nil {
|
||||
source = adapter.nativeDocument[object]
|
||||
}
|
||||
if source == nil {
|
||||
return OCRGeometry{}, ErrObjectNotFound
|
||||
}
|
||||
adapter.refreshForeground()
|
||||
before := adapter.Foreground()
|
||||
if !before.Client.Known || before.Client.Window == 0 || before.Client.PID <= 0 {
|
||||
return OCRGeometry{}, ErrOCRGeometry
|
||||
}
|
||||
// Reject dead source/ancestor content without querying text. OCR reads only
|
||||
// visible pixels, so a password role does not prohibit capture geometry.
|
||||
current := (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(source)))
|
||||
valid := false
|
||||
seen := make(map[ObjectID]bool)
|
||||
for depth := 0; current != nil && depth < 128 && ctx.Err() == nil; depth++ {
|
||||
id := nativeObjectID(current)
|
||||
C.atspi_accessible_clear_cache_single(current)
|
||||
states := C.atspi_accessible_get_state_set(current)
|
||||
safe := states != nil && C.atspi_state_set_contains(states, C.ATSPI_STATE_DEFUNCT) == 0
|
||||
if states != nil {
|
||||
C.g_object_unref(C.gpointer(states))
|
||||
}
|
||||
role := C.atspi_accessible_get_role(current, nil)
|
||||
if !safe || !id.Valid() || seen[id] || role == C.ATSPI_ROLE_INVALID {
|
||||
C.g_object_unref(C.gpointer(current))
|
||||
return OCRGeometry{}, ErrOCRGeometry
|
||||
}
|
||||
seen[id] = true
|
||||
if isWindowRole(role) {
|
||||
valid = true
|
||||
C.g_object_unref(C.gpointer(current))
|
||||
current = nil
|
||||
break
|
||||
}
|
||||
parent := C.atspi_accessible_get_parent(current, nil)
|
||||
C.g_object_unref(C.gpointer(current))
|
||||
current = parent
|
||||
}
|
||||
if current != nil {
|
||||
C.g_object_unref(C.gpointer(current))
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return OCRGeometry{}, err
|
||||
}
|
||||
if !valid {
|
||||
return OCRGeometry{}, ErrOCRGeometry
|
||||
}
|
||||
window := C.cthulhu_window_ancestor(source)
|
||||
if window == nil {
|
||||
return OCRGeometry{}, ErrOCRGeometry
|
||||
}
|
||||
defer C.g_object_unref(C.gpointer(window))
|
||||
if int(C.atspi_accessible_get_process_id(window, nil)) != before.Client.PID || (adapter.foregroundWindow.Valid() && adapter.foregroundWindow != nativeObjectID(window)) {
|
||||
return OCRGeometry{}, ErrOCRGeometry
|
||||
}
|
||||
states := C.atspi_accessible_get_state_set(window)
|
||||
active := states != nil && C.atspi_state_set_contains(states, C.ATSPI_STATE_ACTIVE) != 0 && C.atspi_state_set_contains(states, C.ATSPI_STATE_ICONIFIED) == 0
|
||||
if states != nil {
|
||||
C.g_object_unref(C.gpointer(states))
|
||||
}
|
||||
if !active {
|
||||
return OCRGeometry{}, ErrOCRGeometry
|
||||
}
|
||||
geometry, err := geometryFromNative(ctx, source)
|
||||
if err != nil {
|
||||
return OCRGeometry{}, err
|
||||
}
|
||||
windowGeometry, err := geometryFromNative(ctx, window)
|
||||
if err != nil {
|
||||
return OCRGeometry{}, err
|
||||
}
|
||||
if !geometry.BoundsKnown || !windowGeometry.BoundsKnown {
|
||||
return OCRGeometry{}, ErrOCRGeometry
|
||||
}
|
||||
bounds := geometry.Bounds
|
||||
if zone != nil {
|
||||
clipped, ok := zone.Intersection(bounds)
|
||||
// A stale zone must not move or expand its captured area implicitly.
|
||||
if !ok || clipped != *zone {
|
||||
return OCRGeometry{}, ErrStaleGeometry
|
||||
}
|
||||
bounds = clipped
|
||||
}
|
||||
screen, err := ocr_screen_bounds(bounds, windowGeometry.Bounds, before.Client)
|
||||
adapter.refreshForeground()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return OCRGeometry{}, err
|
||||
}
|
||||
if adapter.Foreground() != before {
|
||||
return OCRGeometry{}, ErrStaleGeometry
|
||||
}
|
||||
if err != nil {
|
||||
return OCRGeometry{}, err
|
||||
}
|
||||
return OCRGeometry{Object: object, ProviderBounds: bounds, WindowBounds: windowGeometry.Bounds, ScreenBounds: screen, Foreground: before}, nil
|
||||
}
|
||||
|
||||
func isWindowRole(role C.AtspiRole) bool {
|
||||
return role == C.ATSPI_ROLE_FRAME || role == C.ATSPI_ROLE_DIALOG || role == C.ATSPI_ROLE_WINDOW
|
||||
}
|
||||
Reference in new issue
Block a user