98 lines
3.7 KiB
Go
98 lines
3.7 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package atspi
|
|
|
|
import (
|
|
"errors"
|
|
"image"
|
|
"math"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/platform/xorg"
|
|
)
|
|
|
|
var ErrOCRGeometry = errors.New("OCR source bounds unavailable")
|
|
|
|
// OCRGeometry is an owned snapshot. ProviderBounds/WindowBounds use AT-SPI
|
|
// coordinates; ScreenBounds uses physical X11 root pixels. It authorizes
|
|
// capture only, never activation. Foreground must still match at capture time.
|
|
type OCRGeometry struct {
|
|
Object ObjectID
|
|
ProviderBounds, WindowBounds Rectangle
|
|
ScreenBounds image.Rectangle
|
|
Foreground ForegroundState
|
|
}
|
|
|
|
// Match the source's own fresh top-level window, allowing the same proportional
|
|
// scaling supported by the capture contract. Origins must match either the
|
|
// physical or uniformly scaled root. Unknown origins are rejected. Use measured
|
|
// per-axis ratios, not DPI.
|
|
func ocr_screen_bounds(bounds, window Rectangle, client xorg.Foreground) (image.Rectangle, error) {
|
|
if !bounds.Valid() || !window.Valid() || !client.Known || client.Window == 0 {
|
|
return image.Rectangle{}, ErrOCRGeometry
|
|
}
|
|
inner := Rectangle{int32(client.X), int32(client.Y), int32(client.Width), int32(client.Height)}
|
|
outer := Rectangle{int32(client.OuterX), int32(client.OuterY), int32(client.OuterWidth), int32(client.OuterHeight)}
|
|
matched := Rectangle{}
|
|
// Exact matches take precedence over a proportional decoration match.
|
|
for _, candidate := range []Rectangle{inner, outer} {
|
|
if window == candidate {
|
|
matched = candidate
|
|
break
|
|
}
|
|
}
|
|
if !matched.Valid() {
|
|
for _, candidate := range []Rectangle{inner, outer} {
|
|
if ocr_window_matches(window, candidate) {
|
|
if matched.Valid() && matched != candidate {
|
|
return image.Rectangle{}, ErrOCRGeometry
|
|
}
|
|
matched = candidate
|
|
}
|
|
}
|
|
}
|
|
if !matched.Valid() {
|
|
return image.Rectangle{}, ErrOCRGeometry
|
|
}
|
|
clipped, ok := bounds.Intersection(window)
|
|
if !ok {
|
|
return image.Rectangle{}, ErrOCRGeometry
|
|
}
|
|
|
|
// Use exact integer edge ratios. Provider windows can exceed the OCR image
|
|
// pixel cap while the chosen object/zone remains a small valid capture.
|
|
map_edge := func(value int64, origin, extent, screenOrigin, screenExtent int32, upper bool) int {
|
|
numerator := (value - int64(origin)) * int64(screenExtent)
|
|
if upper {
|
|
numerator += int64(extent) - 1
|
|
}
|
|
return int(int64(screenOrigin) + numerator/int64(extent))
|
|
}
|
|
mapped := image.Rect(map_edge(int64(clipped.X), window.X, window.Width, matched.X, matched.Width, false),
|
|
map_edge(int64(clipped.Y), window.Y, window.Height, matched.Y, matched.Height, false),
|
|
map_edge(int64(clipped.X)+int64(clipped.Width), window.X, window.Width, matched.X, matched.Width, true),
|
|
map_edge(int64(clipped.Y)+int64(clipped.Height), window.Y, window.Height, matched.Y, matched.Height, true))
|
|
if mapped.Empty() {
|
|
return image.Rectangle{}, ErrOCRGeometry
|
|
}
|
|
|
|
return mapped, nil
|
|
}
|
|
|
|
// GTK can scale the root origin as well as dimensions; Chromium may retain
|
|
// a physical root origin. Admit these two measured contracts only. Other
|
|
// monitor-dependent origin conventions need separate evidence.
|
|
func ocr_window_matches(provider, native Rectangle) bool {
|
|
if !provider.Valid() || !native.Valid() {
|
|
return false
|
|
}
|
|
scaleX, scaleY := float64(native.Width)/float64(provider.Width), float64(native.Height)/float64(provider.Height)
|
|
if scaleX < 0.5 || scaleX > 3 || scaleY < 0.5 || scaleY > 3 || math.Abs(scaleX-scaleY) > 0.01*math.Max(scaleX, scaleY) {
|
|
return false
|
|
}
|
|
return provider.X == native.X && provider.Y == native.Y ||
|
|
math.Abs(float64(provider.X)*scaleX-float64(native.X)) <= 1 &&
|
|
math.Abs(float64(provider.Y)*scaleY-float64(native.Y)) <= 1
|
|
}
|