Files
cthulhu/internal/atspi/adapter_ocr_native.go

148 lines
4.6 KiB
Go

// 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 = &copy
}
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
}