mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-12 09:10:30 +08:00
ocr
This commit is contained in:
parent
67839a52a2
commit
96191a1b08
@ -454,7 +454,8 @@ def _get_config_scheme() -> Dict:
|
|||||||
},
|
},
|
||||||
|
|
||||||
"ocr": {
|
"ocr": {
|
||||||
"langs": Option(["eng"], type=valid_string_list, unpack_as="default_langs"),
|
"langs": Option(["eng"], type=valid_string_list, unpack_as="default_langs"),
|
||||||
|
"tessdata": Option("/usr/share/tessdata", type=valid_stripped_string_not_empty, unpack_as="data_dir_path")
|
||||||
},
|
},
|
||||||
|
|
||||||
"snapshot": {
|
"snapshot": {
|
||||||
|
|||||||
@ -63,7 +63,7 @@ class StreamerApi:
|
|||||||
)
|
)
|
||||||
if snapshot:
|
if snapshot:
|
||||||
if valid_bool(request.query.get("ocr", "false")):
|
if valid_bool(request.query.get("ocr", "false")):
|
||||||
langs = await self.__ocr.get_available_langs()
|
langs = self.__ocr.get_available_langs()
|
||||||
return Response(
|
return Response(
|
||||||
body=(await self.__ocr.recognize(
|
body=(await self.__ocr.recognize(
|
||||||
data=snapshot.data,
|
data=snapshot.data,
|
||||||
@ -107,8 +107,8 @@ class StreamerApi:
|
|||||||
default: List[str] = []
|
default: List[str] = []
|
||||||
available: List[str] = []
|
available: List[str] = []
|
||||||
if enabled:
|
if enabled:
|
||||||
default = await self.__ocr.get_default_langs()
|
default = self.__ocr.get_default_langs()
|
||||||
available = await self.__ocr.get_available_langs()
|
available = self.__ocr.get_available_langs()
|
||||||
return {
|
return {
|
||||||
"ocr": {
|
"ocr": {
|
||||||
"enabled": enabled,
|
"enabled": enabled,
|
||||||
|
|||||||
@ -32,7 +32,6 @@ from typing import List
|
|||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Set
|
from typing import Set
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Awaitable
|
|
||||||
from typing import Coroutine
|
from typing import Coroutine
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@ -264,16 +263,27 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
|
|||||||
await self.__register_ws_client(client)
|
await self.__register_ws_client(client)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.__send_events_aws(client.ws, [
|
stage1 = [
|
||||||
("gpio_model_state", self.__user_gpio.get_model()),
|
("gpio_model_state", self.__user_gpio.get_model()),
|
||||||
("hid_keymaps_state", self.__hid_api.get_keymaps()),
|
("hid_keymaps_state", self.__hid_api.get_keymaps()),
|
||||||
("streamer_ocr_state", self.__streamer_api.get_ocr()),
|
("streamer_ocr_state", self.__streamer_api.get_ocr()),
|
||||||
])
|
]
|
||||||
await self.__send_events_aws(client.ws, [
|
stage2 = [
|
||||||
(comp.event_type, comp.get_state())
|
(comp.event_type, comp.get_state())
|
||||||
for comp in self.__components
|
for comp in self.__components
|
||||||
if comp.get_state
|
if comp.get_state
|
||||||
])
|
]
|
||||||
|
stages = stage1 + stage2
|
||||||
|
events = dict(zip(
|
||||||
|
map(operator.itemgetter(0), stages),
|
||||||
|
await asyncio.gather(*map(operator.itemgetter(1), stages)),
|
||||||
|
))
|
||||||
|
for stage in [stage1, stage2]:
|
||||||
|
await asyncio.gather(*[
|
||||||
|
self.__send_event(client.ws, event_type, events.pop(event_type))
|
||||||
|
for (event_type, _) in stage
|
||||||
|
])
|
||||||
|
|
||||||
await self.__send_event(client.ws, "loop", {})
|
await self.__send_event(client.ws, "loop", {})
|
||||||
|
|
||||||
async for msg in client.ws:
|
async for msg in client.ws:
|
||||||
@ -391,15 +401,6 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
|
|||||||
logger.exception("Cleanup error on %s", comp.name)
|
logger.exception("Cleanup error on %s", comp.name)
|
||||||
logger.info("On-Cleanup complete")
|
logger.info("On-Cleanup complete")
|
||||||
|
|
||||||
async def __send_events_aws(self, ws: aiohttp.web.WebSocketResponse, sources: List[Tuple[str, Awaitable]]) -> None:
|
|
||||||
await asyncio.gather(*[
|
|
||||||
self.__send_event(ws, event_type, state)
|
|
||||||
for (event_type, state) in zip(
|
|
||||||
map(operator.itemgetter(0), sources),
|
|
||||||
await asyncio.gather(*map(operator.itemgetter(1), sources)),
|
|
||||||
)
|
|
||||||
])
|
|
||||||
|
|
||||||
async def __send_event(self, ws: aiohttp.web.WebSocketResponse, event_type: str, event: Optional[Dict]) -> None:
|
async def __send_event(self, ws: aiohttp.web.WebSocketResponse, event_type: str, event: Optional[Dict]) -> None:
|
||||||
await ws.send_str(json.dumps({
|
await ws.send_str(json.dumps({
|
||||||
"event_type": event_type,
|
"event_type": event_type,
|
||||||
|
|||||||
@ -20,6 +20,8 @@
|
|||||||
# ========================================================================== #
|
# ========================================================================== #
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
import io
|
import io
|
||||||
import ctypes
|
import ctypes
|
||||||
import ctypes.util
|
import ctypes.util
|
||||||
@ -69,7 +71,6 @@ def _load_libtesseract() -> Optional[ctypes.CDLL]:
|
|||||||
("TessBaseAPISetImage", None, [POINTER(_TessBaseAPI), c_void_p, c_int, c_int, c_int, c_int]),
|
("TessBaseAPISetImage", None, [POINTER(_TessBaseAPI), c_void_p, c_int, c_int, c_int, c_int]),
|
||||||
("TessBaseAPIGetUTF8Text", POINTER(c_char), [POINTER(_TessBaseAPI)]),
|
("TessBaseAPIGetUTF8Text", POINTER(c_char), [POINTER(_TessBaseAPI)]),
|
||||||
("TessBaseAPISetVariable", c_bool, [POINTER(_TessBaseAPI), c_char_p, c_char_p]),
|
("TessBaseAPISetVariable", c_bool, [POINTER(_TessBaseAPI), c_char_p, c_char_p]),
|
||||||
("TessBaseAPIGetAvailableLanguagesAsVector", POINTER(POINTER(c_char)), [POINTER(_TessBaseAPI)]),
|
|
||||||
]:
|
]:
|
||||||
func = getattr(lib, name)
|
func = getattr(lib, name)
|
||||||
if not func:
|
if not func:
|
||||||
@ -86,12 +87,12 @@ _libtess = _load_libtesseract()
|
|||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def _tess_api(langs: List[str]) -> Generator[_TessBaseAPI, None, None]:
|
def _tess_api(data_dir_path: str, langs: List[str]) -> Generator[_TessBaseAPI, None, None]:
|
||||||
if not _libtess:
|
if not _libtess:
|
||||||
raise OcrError("Tesseract is not available")
|
raise OcrError("Tesseract is not available")
|
||||||
api = _libtess.TessBaseAPICreate()
|
api = _libtess.TessBaseAPICreate()
|
||||||
try:
|
try:
|
||||||
if _libtess.TessBaseAPIInit3(api, None, "+".join(langs).encode()) != 0:
|
if _libtess.TessBaseAPIInit3(api, data_dir_path.encode(), "+".join(langs).encode()) != 0:
|
||||||
raise OcrError("Can't initialize Tesseract")
|
raise OcrError("Can't initialize Tesseract")
|
||||||
if not _libtess.TessBaseAPISetVariable(api, b"debug_file", b"/dev/null"):
|
if not _libtess.TessBaseAPISetVariable(api, b"debug_file", b"/dev/null"):
|
||||||
raise OcrError("Can't set debug_file=/dev/null")
|
raise OcrError("Can't set debug_file=/dev/null")
|
||||||
@ -100,35 +101,32 @@ def _tess_api(langs: List[str]) -> Generator[_TessBaseAPI, None, None]:
|
|||||||
_libtess.TessBaseAPIDelete(api)
|
_libtess.TessBaseAPIDelete(api)
|
||||||
|
|
||||||
|
|
||||||
|
_LANG_SUFFIX = ".traineddata"
|
||||||
|
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
class TesseractOcr:
|
class TesseractOcr:
|
||||||
def __init__(self, default_langs: List[str]) -> None:
|
def __init__(self, data_dir_path: str, default_langs: List[str]) -> None:
|
||||||
|
self.__data_dir_path = data_dir_path
|
||||||
self.__default_langs = default_langs
|
self.__default_langs = default_langs
|
||||||
|
|
||||||
def is_available(self) -> bool:
|
def is_available(self) -> bool:
|
||||||
return bool(_libtess)
|
return bool(_libtess)
|
||||||
|
|
||||||
async def get_default_langs(self) -> List[str]:
|
def get_default_langs(self) -> List[str]:
|
||||||
return list(self.__default_langs)
|
return list(self.__default_langs)
|
||||||
|
|
||||||
async def get_available_langs(self) -> List[str]:
|
def get_available_langs(self) -> List[str]:
|
||||||
return (await aiotools.run_async(self.__inner_get_available_langs))
|
# Это быстрее чем, инициализация либы и TessBaseAPIGetAvailableLanguagesAsVector()
|
||||||
|
langs: Set[str] = set()
|
||||||
def __inner_get_available_langs(self) -> List[str]:
|
for lang_name in os.listdir(self.__data_dir_path):
|
||||||
with _tess_api(["osd"]) as api:
|
if lang_name.endswith(_LANG_SUFFIX):
|
||||||
assert _libtess
|
path = os.path.join(self.__data_dir_path, lang_name)
|
||||||
langs: Set[str] = set()
|
if os.access(path, os.R_OK) and stat.S_ISREG(os.stat(path).st_mode):
|
||||||
langs_ptr = _libtess.TessBaseAPIGetAvailableLanguagesAsVector(api)
|
lang = lang_name[:-len(_LANG_SUFFIX)]
|
||||||
if langs_ptr is not None:
|
if lang:
|
||||||
index = 0
|
langs.add(lang)
|
||||||
while langs_ptr[index]:
|
return sorted(langs)
|
||||||
lang = ctypes.cast(langs_ptr[index], c_char_p).value
|
|
||||||
if lang is not None:
|
|
||||||
langs.add(lang.decode())
|
|
||||||
libc.free(langs_ptr[index])
|
|
||||||
index += 1
|
|
||||||
libc.free(langs_ptr)
|
|
||||||
return sorted(langs)
|
|
||||||
|
|
||||||
async def recognize(self, data: bytes, langs: List[str], left: int, top: int, right: int, bottom: int) -> str:
|
async def recognize(self, data: bytes, langs: List[str], left: int, top: int, right: int, bottom: int) -> str:
|
||||||
if not langs:
|
if not langs:
|
||||||
@ -136,7 +134,7 @@ class TesseractOcr:
|
|||||||
return (await aiotools.run_async(self.__inner_recognize, data, langs, left, top, right, bottom))
|
return (await aiotools.run_async(self.__inner_recognize, data, langs, left, top, right, bottom))
|
||||||
|
|
||||||
def __inner_recognize(self, data: bytes, langs: List[str], left: int, top: int, right: int, bottom: int) -> str:
|
def __inner_recognize(self, data: bytes, langs: List[str], left: int, top: int, right: int, bottom: int) -> str:
|
||||||
with _tess_api(langs) as api:
|
with _tess_api(self.__data_dir_path, langs) as api:
|
||||||
assert _libtess
|
assert _libtess
|
||||||
with io.BytesIO(data) as bio:
|
with io.BytesIO(data) as bio:
|
||||||
image = PilImage.open(bio)
|
image = PilImage.open(bio)
|
||||||
|
|||||||
@ -506,7 +506,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="right"><a class="menu-button" href="#">Paste</a>
|
<li class="right"><a class="menu-button" href="#"><img class="feature-disabled" data-dont-hide-menu id="stream-ocr-led" src="/share/svg/led-gear.svg">Text</a>
|
||||||
<div class="menu" data-dont-hide-menu>
|
<div class="menu" data-dont-hide-menu>
|
||||||
<div class="text"><b>Paste text as keypress sequence<br></b><sub>Please note that PiKVM cannot switch the keyboard layout</sub></div>
|
<div class="text"><b>Paste text as keypress sequence<br></b><sub>Please note that PiKVM cannot switch the keyboard layout</sub></div>
|
||||||
<hr>
|
<hr>
|
||||||
@ -535,6 +535,35 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
<div class="feature-disabled" id="stream-ocr">
|
||||||
|
<hr><br>
|
||||||
|
<hr>
|
||||||
|
<div class="text"><b>Text recognition<br></b><sub><a target="_blank" href="https://docs.pikvm.org/ocr">OCR</a> works locally on PiKVM</sub></div>
|
||||||
|
<hr>
|
||||||
|
<table class="kv">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<button data-force-hide-menu id="stream-ocr-button">• Select area</button>
|
||||||
|
</td>
|
||||||
|
<td>for</td>
|
||||||
|
<td>
|
||||||
|
<select id="stream-ocr-lang-selector"></select>
|
||||||
|
</td>
|
||||||
|
<td>text recognition</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table class="kv">
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">• Press <b>Enter</b> to recognize and copy text to clipboard</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">• Press <b>Esc</b> to cancel selection</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="right"><a class="menu-button" href="#">Shortcuts</a>
|
<li class="right"><a class="menu-button" href="#">Shortcuts</a>
|
||||||
@ -588,6 +617,9 @@
|
|||||||
<div class="menu" data-dont-hide-menu id="gpio-menu"></div>
|
<div class="menu" data-dont-hide-menu id="gpio-menu"></div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<div class="window" id="stream-ocr-window">
|
||||||
|
<div class="hidden" id="stream-ocr-selection"></div>
|
||||||
|
</div>
|
||||||
<div class="window window-resizable" id="stream-window">
|
<div class="window window-resizable" id="stream-window">
|
||||||
<div class="window-header" id="stream-window-header">
|
<div class="window-header" id="stream-window-header">
|
||||||
<div class="window-grab">MJPEG</div>
|
<div class="window-grab">MJPEG</div>
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
li(class="right")
|
|
||||||
a(class="menu-button" href="#") Paste
|
|
||||||
div(data-dont-hide-menu class="menu")
|
|
||||||
div(class="text")
|
|
||||||
b Paste text as keypress sequence#[br]
|
|
||||||
sub Please note that PiKVM cannot switch the keyboard layout
|
|
||||||
hr
|
|
||||||
div(class="text")
|
|
||||||
textarea(id="hid-pak-text" placeholder="Enter your text here")
|
|
||||||
table(class="kv")
|
|
||||||
tr
|
|
||||||
td
|
|
||||||
button(disabled data-force-hide-menu id="hid-pak-button") • Paste
|
|
||||||
td using host keymap
|
|
||||||
td
|
|
||||||
select(id="hid-pak-keymap-selector")
|
|
||||||
+menu_switch("hid-pak-ask-switch", "Ask paste confirmation", true, true)
|
|
||||||
42
web/kvm/navbar-text.pug
Normal file
42
web/kvm/navbar-text.pug
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
li(class="right")
|
||||||
|
a(class="menu-button" href="#")
|
||||||
|
+navbar_led("stream-ocr-led", "led-gear", "feature-disabled")
|
||||||
|
| Text
|
||||||
|
div(data-dont-hide-menu class="menu")
|
||||||
|
div(class="text")
|
||||||
|
b Paste text as keypress sequence#[br]
|
||||||
|
sub Please note that PiKVM cannot switch the keyboard layout
|
||||||
|
hr
|
||||||
|
div(class="text")
|
||||||
|
textarea(id="hid-pak-text" placeholder="Enter your text here")
|
||||||
|
table(class="kv")
|
||||||
|
tr
|
||||||
|
td
|
||||||
|
button(disabled data-force-hide-menu id="hid-pak-button") • Paste
|
||||||
|
td using host keymap
|
||||||
|
td
|
||||||
|
select(id="hid-pak-keymap-selector")
|
||||||
|
+menu_switch("hid-pak-ask-switch", "Ask paste confirmation", true, true)
|
||||||
|
div(id="stream-ocr" class="feature-disabled")
|
||||||
|
hr
|
||||||
|
br
|
||||||
|
hr
|
||||||
|
div(class="text")
|
||||||
|
b Text recognition#[br]
|
||||||
|
sub #[a(target="_blank" href="https://docs.pikvm.org/ocr") OCR] works locally on PiKVM
|
||||||
|
hr
|
||||||
|
table(class="kv")
|
||||||
|
tr
|
||||||
|
td
|
||||||
|
button(data-force-hide-menu id="stream-ocr-button") • Select area
|
||||||
|
td for
|
||||||
|
td
|
||||||
|
select(id="stream-ocr-lang-selector")
|
||||||
|
td text recognition
|
||||||
|
table(class="kv")
|
||||||
|
tr
|
||||||
|
td(colspan="4") • Press #[b Enter] to recognize and copy text to clipboard
|
||||||
|
tr
|
||||||
|
td(colspan="4") • Press #[b Esc] to cancel selection
|
||||||
|
tr
|
||||||
|
td
|
||||||
@ -38,6 +38,6 @@ ul(id="navbar")
|
|||||||
include navbar-atx.pug
|
include navbar-atx.pug
|
||||||
include navbar-msd.pug
|
include navbar-msd.pug
|
||||||
include navbar-macro.pug
|
include navbar-macro.pug
|
||||||
include navbar-paste.pug
|
include navbar-text.pug
|
||||||
include navbar-shortcuts.pug
|
include navbar-shortcuts.pug
|
||||||
include navbar-gpio.pug
|
include navbar-gpio.pug
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
div(id="stream-ocr-window" class="window")
|
||||||
|
div(id="stream-ocr-selection" class="hidden")
|
||||||
|
|
||||||
div(id="stream-window" class="window window-resizable")
|
div(id="stream-window" class="window window-resizable")
|
||||||
div(id="stream-window-header" class="window-header")
|
div(id="stream-window-header" class="window-header")
|
||||||
div(class="window-grab") MJPEG
|
div(class="window-grab") MJPEG
|
||||||
|
|||||||
@ -23,8 +23,8 @@
|
|||||||
textarea#hid-pak-text {
|
textarea#hid-pak-text {
|
||||||
display: block;
|
display: block;
|
||||||
resize: none;
|
resize: none;
|
||||||
height: 150px;
|
height: 120px;
|
||||||
width: 300px;
|
width: 320px;
|
||||||
border: var(--border-default-thin);
|
border: var(--border-default-thin);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--cs-code-default-fg);
|
color: var(--cs-code-default-fg);
|
||||||
|
|||||||
@ -29,6 +29,26 @@ div#stream-info {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div#stream-ocr-window {
|
||||||
|
cursor: crosshair;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background-color: unset !important;
|
||||||
|
border-radius: unset !important;
|
||||||
|
border: unset !important;
|
||||||
|
padding: 0px !important;
|
||||||
|
background: radial-gradient(transparent 15%, black);
|
||||||
|
}
|
||||||
|
div#stream-ocr-selection {
|
||||||
|
position: relative;
|
||||||
|
background-color: #5b90bb50;
|
||||||
|
box-shadow: inset 0 0 0px 1px #e8e8e8cd;
|
||||||
|
}
|
||||||
|
|
||||||
div#stream-box {
|
div#stream-box {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@ -30,7 +30,7 @@ import {Keyboard} from "./keyboard.js";
|
|||||||
import {Mouse} from "./mouse.js";
|
import {Mouse} from "./mouse.js";
|
||||||
|
|
||||||
|
|
||||||
export function Hid(__getResolution, __recorder) {
|
export function Hid(__getGeometry, __recorder) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
/************************************************************************/
|
/************************************************************************/
|
||||||
@ -40,7 +40,7 @@ export function Hid(__getResolution, __recorder) {
|
|||||||
|
|
||||||
var __init__ = function() {
|
var __init__ = function() {
|
||||||
__keyboard = new Keyboard(__recorder.recordWsEvent);
|
__keyboard = new Keyboard(__recorder.recordWsEvent);
|
||||||
__mouse = new Mouse(__getResolution, __recorder.recordWsEvent);
|
__mouse = new Mouse(__getGeometry, __recorder.recordWsEvent);
|
||||||
|
|
||||||
let hidden_attr = null;
|
let hidden_attr = null;
|
||||||
let visibility_change_attr = null;
|
let visibility_change_attr = null;
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import {tools, $} from "../tools.js";
|
|||||||
import {Keypad} from "../keypad.js";
|
import {Keypad} from "../keypad.js";
|
||||||
|
|
||||||
|
|
||||||
export function Mouse(__getResolution, __recordWsEvent) {
|
export function Mouse(__getGeometry, __recordWsEvent) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
/************************************************************************/
|
/************************************************************************/
|
||||||
@ -227,10 +227,10 @@ export function Mouse(__getResolution, __recordWsEvent) {
|
|||||||
if (__absolute) {
|
if (__absolute) {
|
||||||
let pos = __current_pos;
|
let pos = __current_pos;
|
||||||
if (pos.x !== __sent_pos.x || pos.y !== __sent_pos.y) {
|
if (pos.x !== __sent_pos.x || pos.y !== __sent_pos.y) {
|
||||||
let geo = __getVideoGeometry();
|
let geo = __getGeometry();
|
||||||
let to = {
|
let to = {
|
||||||
"x": __translatePosition(pos.x, geo.x, geo.width, -32768, 32767),
|
"x": tools.remap(pos.x, geo.x, geo.width, -32768, 32767),
|
||||||
"y": __translatePosition(pos.y, geo.y, geo.height, -32768, 32767),
|
"y": tools.remap(pos.y, geo.y, geo.height, -32768, 32767),
|
||||||
};
|
};
|
||||||
tools.debug("Mouse: moved:", to);
|
tools.debug("Mouse: moved:", to);
|
||||||
__sendEvent("mouse_move", {"to": to});
|
__sendEvent("mouse_move", {"to": to});
|
||||||
@ -243,36 +243,6 @@ export function Mouse(__getResolution, __recordWsEvent) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var __getVideoGeometry = function() {
|
|
||||||
// Первоначально обновление геометрии считалось через ResizeObserver.
|
|
||||||
// Но оно не ловило некоторые события, например в последовательности:
|
|
||||||
// - Находять в HD переходим в фулскрин
|
|
||||||
// - Меняем разрешение на маленькое
|
|
||||||
// - Убираем фулскрин
|
|
||||||
// - Переходим в HD
|
|
||||||
// - Видим нарушение пропорций
|
|
||||||
// Так что теперь используются быстре рассчеты через offset*
|
|
||||||
// вместо getBoundingClientRect().
|
|
||||||
let res = __getResolution();
|
|
||||||
let ratio = Math.min(res.view_width / res.real_width, res.view_height / res.real_height);
|
|
||||||
return {
|
|
||||||
"x": Math.round((res.view_width - ratio * res.real_width) / 2),
|
|
||||||
"y": Math.round((res.view_height - ratio * res.real_height) / 2),
|
|
||||||
"width": Math.round(ratio * res.real_width),
|
|
||||||
"height": Math.round(ratio * res.real_height),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
var __translatePosition = function(x, a, b, c, d) {
|
|
||||||
let translated = Math.round((x - a) / b * (d - c) + c);
|
|
||||||
if (translated < c) {
|
|
||||||
return c;
|
|
||||||
} else if (translated > d) {
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
return translated;
|
|
||||||
};
|
|
||||||
|
|
||||||
var __streamWheelHandler = function(event) {
|
var __streamWheelHandler = function(event) {
|
||||||
// https://learn.javascript.ru/mousewheel
|
// https://learn.javascript.ru/mousewheel
|
||||||
// https://stackoverflow.com/a/24595588
|
// https://stackoverflow.com/a/24595588
|
||||||
|
|||||||
181
web/share/js/kvm/ocr.js
Normal file
181
web/share/js/kvm/ocr.js
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
# #
|
||||||
|
# KVMD - The main PiKVM daemon. #
|
||||||
|
# #
|
||||||
|
# Copyright (C) 2018-2022 Maxim Devaev <mdevaev@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This program is free software: you can redistribute it and/or modify #
|
||||||
|
# it under the terms of the GNU General Public License as published by #
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or #
|
||||||
|
# (at your option) any later version. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, #
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||||
|
# GNU General Public License for more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License #
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
|
# #
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
|
||||||
|
import {tools, $} from "../tools.js";
|
||||||
|
import {wm} from "../wm.js";
|
||||||
|
|
||||||
|
|
||||||
|
export function Ocr(__getGeometry) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
/************************************************************************/
|
||||||
|
|
||||||
|
var __start_pos = null;
|
||||||
|
var __end_pos = null;
|
||||||
|
var __selection = null;
|
||||||
|
|
||||||
|
var __init__ = function() {
|
||||||
|
tools.el.setOnClick($("stream-ocr-button"), function() {
|
||||||
|
__resetSelection();
|
||||||
|
wm.showWindow($("stream-window"));
|
||||||
|
wm.showWindow($("stream-ocr-window"));
|
||||||
|
});
|
||||||
|
|
||||||
|
$("stream-ocr-lang-selector").addEventListener("change", function() {
|
||||||
|
tools.storage.set("stream.ocr.lang", $("stream-ocr-lang-selector").value);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("stream-ocr-window").addEventListener("blur", __resetSelection);
|
||||||
|
$("stream-ocr-window").addEventListener("resize", __resetSelection);
|
||||||
|
$("stream-ocr-window").close_hook = __resetSelection;
|
||||||
|
|
||||||
|
$("stream-ocr-window").onkeyup = function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.code === "Enter") {
|
||||||
|
__recognizeSelection();
|
||||||
|
wm.closeWindow($("stream-ocr-window"));
|
||||||
|
} else if (event.code === "Escape") {
|
||||||
|
wm.closeWindow($("stream-ocr-window"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$("stream-ocr-window").onmousedown = __startSelection;
|
||||||
|
$("stream-ocr-window").onmousemove = __changeSelection;
|
||||||
|
$("stream-ocr-window").onmouseup = __endSelection;
|
||||||
|
};
|
||||||
|
|
||||||
|
/************************************************************************/
|
||||||
|
|
||||||
|
self.setState = function(state) {
|
||||||
|
let enabled = (state && state.ocr.enabled && navigator.clipboard && !tools.browser.is_ios);
|
||||||
|
if (enabled) {
|
||||||
|
let selected = tools.storage.get("stream.ocr.lang", state.ocr.langs["default"]);
|
||||||
|
let html = "";
|
||||||
|
for (let variant of state.ocr.langs.available) {
|
||||||
|
html += `<option value=${variant} ${variant === selected ? "selected" : ""}>${variant}</option>`;
|
||||||
|
}
|
||||||
|
$("stream-ocr-lang-selector").innerHTML = html;
|
||||||
|
}
|
||||||
|
tools.feature.setEnabled($("stream-ocr"), enabled);
|
||||||
|
$("stream-ocr-led").className = (enabled ? "led-gray" : "hidden");
|
||||||
|
};
|
||||||
|
|
||||||
|
var __startSelection = function(event) {
|
||||||
|
if (__start_pos === null) {
|
||||||
|
tools.hidden.setVisible($("stream-ocr-selection"), false);
|
||||||
|
__start_pos = __getGlobalPosition(event);
|
||||||
|
__end_pos = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var __changeSelection = function(event) {
|
||||||
|
if (__start_pos !== null) {
|
||||||
|
__end_pos = __getGlobalPosition(event);
|
||||||
|
let width = Math.abs(__start_pos.x - __end_pos.x);
|
||||||
|
let height = Math.abs(__start_pos.y - __end_pos.y);
|
||||||
|
let el_selection = $("stream-ocr-selection");
|
||||||
|
el_selection.style.left = Math.min(__start_pos.x, __end_pos.x) + "px";
|
||||||
|
el_selection.style.top = Math.min(__start_pos.y, __end_pos.y) + "px";
|
||||||
|
el_selection.style.width = width + "px";
|
||||||
|
el_selection.style.height = height + "px";
|
||||||
|
tools.hidden.setVisible(el_selection, (width > 1 || height > 1));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var __endSelection = function(event) {
|
||||||
|
__changeSelection(event);
|
||||||
|
let el_selection = $("stream-ocr-selection");
|
||||||
|
let ok = (
|
||||||
|
el_selection.offsetWidth > 1 && el_selection.offsetHeight > 1
|
||||||
|
&& __start_pos !== null && __end_pos !== null
|
||||||
|
);
|
||||||
|
tools.hidden.setVisible(el_selection, ok);
|
||||||
|
if (ok) {
|
||||||
|
let rect = $("stream-box").getBoundingClientRect();
|
||||||
|
let rel_left = Math.min(__start_pos.x, __end_pos.x) - rect.left;
|
||||||
|
let rel_right = Math.max(__start_pos.x, __end_pos.x) - rect.left;
|
||||||
|
let rel_top = Math.min(__start_pos.y, __end_pos.y) - rect.top;
|
||||||
|
let rel_bottom = Math.max(__start_pos.y, __end_pos.y) - rect.top;
|
||||||
|
let geo = __getGeometry();
|
||||||
|
__selection = {
|
||||||
|
left: tools.remap(rel_left, geo.x, geo.width, 0, geo.real_width),
|
||||||
|
right: tools.remap(rel_right, geo.x, geo.width, 0, geo.real_width),
|
||||||
|
top: tools.remap(rel_top, geo.y, geo.height, 0, geo.real_height),
|
||||||
|
bottom: tools.remap(rel_bottom, geo.y, geo.height, 0, geo.real_height),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
__selection = null;
|
||||||
|
}
|
||||||
|
__start_pos = null;
|
||||||
|
__end_pos = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
var __getGlobalPosition = function(event) {
|
||||||
|
let rect = $("stream-box").getBoundingClientRect();
|
||||||
|
let geo = __getGeometry();
|
||||||
|
return {
|
||||||
|
x: Math.min(Math.max(event.clientX, rect.left + geo.x), rect.right - geo.x),
|
||||||
|
y: Math.min(Math.max(event.clientY, rect.top + geo.y), rect.bottom - geo.y),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var __resetSelection = function() {
|
||||||
|
tools.hidden.setVisible($("stream-ocr-selection"), false);
|
||||||
|
__start_pos = null;
|
||||||
|
__end_pos = null;
|
||||||
|
__selection = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
var __recognizeSelection = function() {
|
||||||
|
tools.el.setEnabled($("stream-ocr-button"), false);
|
||||||
|
tools.el.setEnabled($("stream-ocr-lang-selector"), false);
|
||||||
|
$("stream-ocr-led").className = "led-yellow-rotating-fast";
|
||||||
|
|
||||||
|
let lang = $("stream-ocr-lang-selector").value;
|
||||||
|
let url = `/api/streamer/snapshot?ocr=1&ocr_lang=${lang}`;
|
||||||
|
url += `&ocr_left=${__selection.left}&ocr_top=${__selection.top}`;
|
||||||
|
url += `&ocr_right=${__selection.right}&ocr_bottom=${__selection.bottom}`;
|
||||||
|
|
||||||
|
let http = tools.makeRequest("GET", url, function() {
|
||||||
|
if (http.readyState === 4) {
|
||||||
|
if (http.status === 200) {
|
||||||
|
navigator.clipboard.writeText(http.responseText).then(function() {
|
||||||
|
wm.info("The text is copied to the clipboard");
|
||||||
|
}, function() {
|
||||||
|
wm.error("Can't copy text to the clipboard");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
wm.error("OCR error:<br>", http.responseText);
|
||||||
|
}
|
||||||
|
|
||||||
|
tools.el.setEnabled($("stream-ocr-button"), true);
|
||||||
|
tools.el.setEnabled($("stream-ocr-lang-selector"), true);
|
||||||
|
$("stream-ocr-led").className = "led-gray";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
__init__();
|
||||||
|
}
|
||||||
@ -32,6 +32,7 @@ import {Atx} from "./atx.js";
|
|||||||
import {Msd} from "./msd.js";
|
import {Msd} from "./msd.js";
|
||||||
import {Streamer} from "./stream.js";
|
import {Streamer} from "./stream.js";
|
||||||
import {Gpio} from "./gpio.js";
|
import {Gpio} from "./gpio.js";
|
||||||
|
import {Ocr} from "./ocr.js";
|
||||||
|
|
||||||
|
|
||||||
export function Session() {
|
export function Session() {
|
||||||
@ -46,10 +47,11 @@ export function Session() {
|
|||||||
|
|
||||||
var __streamer = new Streamer();
|
var __streamer = new Streamer();
|
||||||
var __recorder = new Recorder();
|
var __recorder = new Recorder();
|
||||||
var __hid = new Hid(__streamer.getResolution, __recorder);
|
var __hid = new Hid(__streamer.getGeometry, __recorder);
|
||||||
var __atx = new Atx(__recorder);
|
var __atx = new Atx(__recorder);
|
||||||
var __msd = new Msd();
|
var __msd = new Msd();
|
||||||
var __gpio = new Gpio(__recorder);
|
var __gpio = new Gpio(__recorder);
|
||||||
|
var __ocr = new Ocr(__streamer.getGeometry);
|
||||||
|
|
||||||
var __init__ = function() {
|
var __init__ = function() {
|
||||||
__startSession();
|
__startSession();
|
||||||
@ -251,6 +253,7 @@ export function Session() {
|
|||||||
case "atx_state": __atx.setState(data.event); break;
|
case "atx_state": __atx.setState(data.event); break;
|
||||||
case "msd_state": __msd.setState(data.event); break;
|
case "msd_state": __msd.setState(data.event); break;
|
||||||
case "streamer_state": __streamer.setState(data.event); break;
|
case "streamer_state": __streamer.setState(data.event); break;
|
||||||
|
case "streamer_ocr_state": __ocr.setState(data.event); break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -273,6 +276,7 @@ export function Session() {
|
|||||||
__ping_timer = null;
|
__ping_timer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
__ocr.setState(null);
|
||||||
__gpio.setState(null);
|
__gpio.setState(null);
|
||||||
__hid.setSocket(null);
|
__hid.setSocket(null);
|
||||||
__recorder.setSocket(null);
|
__recorder.setSocket(null);
|
||||||
|
|||||||
@ -455,8 +455,26 @@ export function Streamer() {
|
|||||||
|
|
||||||
/************************************************************************/
|
/************************************************************************/
|
||||||
|
|
||||||
self.getResolution = function() {
|
self.getGeometry = function() {
|
||||||
return __streamer.getResolution();
|
// Первоначально обновление геометрии считалось через ResizeObserver.
|
||||||
|
// Но оно не ловило некоторые события, например в последовательности:
|
||||||
|
// - Находять в HD переходим в фулскрин
|
||||||
|
// - Меняем разрешение на маленькое
|
||||||
|
// - Убираем фулскрин
|
||||||
|
// - Переходим в HD
|
||||||
|
// - Видим нарушение пропорций
|
||||||
|
// Так что теперь используются быстре рассчеты через offset*
|
||||||
|
// вместо getBoundingClientRect().
|
||||||
|
let res = __streamer.getResolution();
|
||||||
|
let ratio = Math.min(res.view_width / res.real_width, res.view_height / res.real_height);
|
||||||
|
return {
|
||||||
|
"x": Math.round((res.view_width - ratio * res.real_width) / 2),
|
||||||
|
"y": Math.round((res.view_height - ratio * res.real_height) / 2),
|
||||||
|
"width": Math.round(ratio * res.real_width),
|
||||||
|
"height": Math.round(ratio * res.real_height),
|
||||||
|
"real_width": res.real_width,
|
||||||
|
"real_height": res.real_height,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
self.setJanusEnabled = function(enabled) {
|
self.setJanusEnabled = function(enabled) {
|
||||||
|
|||||||
@ -83,6 +83,16 @@ export var tools = new function() {
|
|||||||
return `${hours}:${mins}:${secs}.${millis}`;
|
return `${hours}:${mins}:${secs}.${millis}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.remap = function(x, a1, b1, a2, b2) {
|
||||||
|
let remapped = Math.round((x - a1) / b1 * (b2 - a2) + a2);
|
||||||
|
if (remapped < a2) {
|
||||||
|
return a2;
|
||||||
|
} else if (remapped > b2) {
|
||||||
|
return b2;
|
||||||
|
}
|
||||||
|
return remapped;
|
||||||
|
};
|
||||||
|
|
||||||
/************************************************************************/
|
/************************************************************************/
|
||||||
|
|
||||||
self.el = new function() {
|
self.el = new function() {
|
||||||
|
|||||||
@ -84,10 +84,7 @@ function __WindowManager() {
|
|||||||
let el_close_button = el_window.querySelector(".window-header .window-button-close");
|
let el_close_button = el_window.querySelector(".window-header .window-button-close");
|
||||||
if (el_close_button) {
|
if (el_close_button) {
|
||||||
el_close_button.title = "Close window";
|
el_close_button.title = "Close window";
|
||||||
tools.el.setOnClick(el_close_button, function() {
|
tools.el.setOnClick(el_close_button, () => self.closeWindow(el_window));
|
||||||
__closeWindow(el_window);
|
|
||||||
__activateLastWindow(el_window);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let el_maximize_button = el_window.querySelector(".window-header .window-button-maximize");
|
let el_maximize_button = el_window.querySelector(".window-header .window-button-maximize");
|
||||||
@ -139,6 +136,7 @@ function __WindowManager() {
|
|||||||
|
|
||||||
/************************************************************************/
|
/************************************************************************/
|
||||||
|
|
||||||
|
self.info = (...args) => __modalDialog("Info", args.join(" "), true, false, null);
|
||||||
self.error = (...args) => __modalDialog("Error", args.join(" "), true, false, null);
|
self.error = (...args) => __modalDialog("Error", args.join(" "), true, false, null);
|
||||||
self.confirm = (...args) => __modalDialog("Question", args.join(" "), true, true, null);
|
self.confirm = (...args) => __modalDialog("Question", args.join(" "), true, true, null);
|
||||||
|
|
||||||
@ -253,6 +251,11 @@ function __WindowManager() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.closeWindow = function(el_window) {
|
||||||
|
__closeWindow(el_window);
|
||||||
|
__activateLastWindow(el_window);
|
||||||
|
};
|
||||||
|
|
||||||
var __closeWindow = function(el_window) {
|
var __closeWindow = function(el_window) {
|
||||||
el_window.focus();
|
el_window.focus();
|
||||||
el_window.blur();
|
el_window.blur();
|
||||||
@ -460,6 +463,10 @@ function __WindowManager() {
|
|||||||
var __makeWindowMovable = function(el_window) {
|
var __makeWindowMovable = function(el_window) {
|
||||||
let el_header = el_window.querySelector(".window-header");
|
let el_header = el_window.querySelector(".window-header");
|
||||||
let el_grab = el_window.querySelector(".window-header .window-grab");
|
let el_grab = el_window.querySelector(".window-header .window-grab");
|
||||||
|
if (el_header === null || el_grab === null) {
|
||||||
|
// Для псевдоокна OCR
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let prev_pos = {x: 0, y: 0};
|
let prev_pos = {x: 0, y: 0};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user