148 lines
4.6 KiB
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 = ©
|
|
}
|
|
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
|
|
}
|