summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--kvmd/apps/__init__.py3
-rw-r--r--kvmd/apps/kvmd/api/streamer.py6
-rw-r--r--kvmd/apps/kvmd/server.py29
-rw-r--r--kvmd/apps/kvmd/tesseract.py46
-rw-r--r--web/kvm/index.html34
-rw-r--r--web/kvm/navbar-paste.pug17
-rw-r--r--web/kvm/navbar-text.pug42
-rw-r--r--web/kvm/navbar.pug2
-rw-r--r--web/kvm/window-stream.pug3
-rw-r--r--web/share/css/kvm/hid.css4
-rw-r--r--web/share/css/kvm/stream.css20
-rw-r--r--web/share/js/kvm/hid.js4
-rw-r--r--web/share/js/kvm/mouse.js38
-rw-r--r--web/share/js/kvm/ocr.js181
-rw-r--r--web/share/js/kvm/session.js6
-rw-r--r--web/share/js/kvm/stream.js22
-rw-r--r--web/share/js/tools.js10
-rw-r--r--web/share/js/wm.js15
18 files changed, 376 insertions, 106 deletions
diff --git a/kvmd/apps/__init__.py b/kvmd/apps/__init__.py
index d40e2feb..d3c8d5bc 100644
--- a/kvmd/apps/__init__.py
+++ b/kvmd/apps/__init__.py
@@ -454,7 +454,8 @@ def _get_config_scheme() -> Dict:
},
"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": {
diff --git a/kvmd/apps/kvmd/api/streamer.py b/kvmd/apps/kvmd/api/streamer.py
index f1ac979a..a2cf961d 100644
--- a/kvmd/apps/kvmd/api/streamer.py
+++ b/kvmd/apps/kvmd/api/streamer.py
@@ -63,7 +63,7 @@ class StreamerApi:
)
if snapshot:
if valid_bool(request.query.get("ocr", "false")):
- langs = await self.__ocr.get_available_langs()
+ langs = self.__ocr.get_available_langs()
return Response(
body=(await self.__ocr.recognize(
data=snapshot.data,
@@ -107,8 +107,8 @@ class StreamerApi:
default: List[str] = []
available: List[str] = []
if enabled:
- default = await self.__ocr.get_default_langs()
- available = await self.__ocr.get_available_langs()
+ default = self.__ocr.get_default_langs()
+ available = self.__ocr.get_available_langs()
return {
"ocr": {
"enabled": enabled,
diff --git a/kvmd/apps/kvmd/server.py b/kvmd/apps/kvmd/server.py
index 7ac422f8..5f007480 100644
--- a/kvmd/apps/kvmd/server.py
+++ b/kvmd/apps/kvmd/server.py
@@ -32,7 +32,6 @@ from typing import List
from typing import Dict
from typing import Set
from typing import Callable
-from typing import Awaitable
from typing import Coroutine
from typing import AsyncGenerator
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)
try:
- await self.__send_events_aws(client.ws, [
+ stage1 = [
("gpio_model_state", self.__user_gpio.get_model()),
("hid_keymaps_state", self.__hid_api.get_keymaps()),
("streamer_ocr_state", self.__streamer_api.get_ocr()),
- ])
- await self.__send_events_aws(client.ws, [
+ ]
+ stage2 = [
(comp.event_type, comp.get_state())
for comp in self.__components
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", {})
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.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:
await ws.send_str(json.dumps({
"event_type": event_type,
diff --git a/kvmd/apps/kvmd/tesseract.py b/kvmd/apps/kvmd/tesseract.py
index 76d52426..ae40c756 100644
--- a/kvmd/apps/kvmd/tesseract.py
+++ b/kvmd/apps/kvmd/tesseract.py
@@ -20,6 +20,8 @@
# ========================================================================== #
+import os
+import stat
import io
import ctypes
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]),
("TessBaseAPIGetUTF8Text", POINTER(c_char), [POINTER(_TessBaseAPI)]),
("TessBaseAPISetVariable", c_bool, [POINTER(_TessBaseAPI), c_char_p, c_char_p]),
- ("TessBaseAPIGetAvailableLanguagesAsVector", POINTER(POINTER(c_char)), [POINTER(_TessBaseAPI)]),
]:
func = getattr(lib, name)
if not func:
@@ -86,12 +87,12 @@ _libtess = _load_libtesseract()
@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:
raise OcrError("Tesseract is not available")
api = _libtess.TessBaseAPICreate()
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")
if not _libtess.TessBaseAPISetVariable(api, b"debug_file", b"/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)
+_LANG_SUFFIX = ".traineddata"
+
+
# =====
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
def is_available(self) -> bool:
return bool(_libtess)
- async def get_default_langs(self) -> List[str]:
+ def get_default_langs(self) -> List[str]:
return list(self.__default_langs)
- async def get_available_langs(self) -> List[str]:
- return (await aiotools.run_async(self.__inner_get_available_langs))
-
- def __inner_get_available_langs(self) -> List[str]:
- with _tess_api(["osd"]) as api:
- assert _libtess
- langs: Set[str] = set()
- langs_ptr = _libtess.TessBaseAPIGetAvailableLanguagesAsVector(api)
- if langs_ptr is not None:
- index = 0
- while langs_ptr[index]:
- 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)
+ def get_available_langs(self) -> List[str]:
+ # Это быстрее чем, инициализация либы и TessBaseAPIGetAvailableLanguagesAsVector()
+ langs: Set[str] = set()
+ for lang_name in os.listdir(self.__data_dir_path):
+ if lang_name.endswith(_LANG_SUFFIX):
+ path = os.path.join(self.__data_dir_path, lang_name)
+ if os.access(path, os.R_OK) and stat.S_ISREG(os.stat(path).st_mode):
+ lang = lang_name[:-len(_LANG_SUFFIX)]
+ if lang:
+ langs.add(lang)
+ return sorted(langs)
async def recognize(self, data: bytes, langs: List[str], left: int, top: int, right: int, bottom: int) -> str:
if not langs:
@@ -136,7 +134,7 @@ class TesseractOcr:
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:
- with _tess_api(langs) as api:
+ with _tess_api(self.__data_dir_path, langs) as api:
assert _libtess
with io.BytesIO(data) as bio:
image = PilImage.open(bio)
diff --git a/web/kvm/index.html b/web/kvm/index.html
index 697a8d5f..d095ed56 100644
--- a/web/kvm/index.html
+++ b/web/kvm/index.html
@@ -506,7 +506,7 @@
</div>
</div>
</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="text"><b>Paste text as keypress sequence<br></b><sub>Please note that PiKVM cannot switch the keyboard layout</sub></div>
<hr>
@@ -535,6 +535,35 @@
</td>
</tr>
</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">&bull; 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">&bull; Press <b>Enter</b> to recognize and copy text to clipboard</td>
+ </tr>
+ <tr>
+ <td colspan="4">&bull; Press <b>Esc</b> to cancel selection</td>
+ </tr>
+ <tr>
+ <td></td>
+ </tr>
+ </table>
+ </div>
</div>
</li>
<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>
</li>
</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-header" id="stream-window-header">
<div class="window-grab">MJPEG</div>
diff --git a/web/kvm/navbar-paste.pug b/web/kvm/navbar-paste.pug
deleted file mode 100644
index 44feac87..00000000
--- a/web/kvm/navbar-paste.pug
+++ /dev/null
@@ -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") &bull; Paste
- td using host keymap
- td
- select(id="hid-pak-keymap-selector")
- +menu_switch("hid-pak-ask-switch", "Ask paste confirmation", true, true)
diff --git a/web/kvm/navbar-text.pug b/web/kvm/navbar-text.pug
new file mode 100644
index 00000000..4fe4f38d
--- /dev/null
+++ b/web/kvm/navbar-text.pug
@@ -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") &bull; 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") &bull; Select area
+ td for
+ td
+ select(id="stream-ocr-lang-selector")
+ td text recognition
+ table(class="kv")
+ tr
+ td(colspan="4") &bull; Press #[b Enter] to recognize and copy text to clipboard
+ tr
+ td(colspan="4") &bull; Press #[b Esc] to cancel selection
+ tr
+ td
diff --git a/web/kvm/navbar.pug b/web/kvm/navbar.pug
index cae4d8e9..3974e319 100644
--- a/web/kvm/navbar.pug
+++ b/web/kvm/navbar.pug
@@ -38,6 +38,6 @@ ul(id="navbar")
include navbar-atx.pug
include navbar-msd.pug
include navbar-macro.pug
- include navbar-paste.pug
+ include navbar-text.pug
include navbar-shortcuts.pug
include navbar-gpio.pug
diff --git a/web/kvm/window-stream.pug b/web/kvm/window-stream.pug
index 36bca291..2520b2b5 100644
--- a/web/kvm/window-stream.pug
+++ b/web/kvm/window-stream.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-header" class="window-header")
div(class="window-grab") MJPEG
diff --git a/web/share/css/kvm/hid.css b/web/share/css/kvm/hid.css
index 02d2f8a3..e27812ce 100644
--- a/web/share/css/kvm/hid.css
+++ b/web/share/css/kvm/hid.css
@@ -23,8 +23,8 @@
textarea#hid-pak-text {
display: block;
resize: none;
- height: 150px;
- width: 300px;
+ height: 120px;
+ width: 320px;
border: var(--border-default-thin);
border-radius: 4px;
color: var(--cs-code-default-fg);
diff --git a/web/share/css/kvm/stream.css b/web/share/css/kvm/stream.css
index a641e296..0ead41cb 100644
--- a/web/share/css/kvm/stream.css
+++ b/web/share/css/kvm/stream.css
@@ -29,6 +29,26 @@ div#stream-info {
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 {
width: 100%;
height: 100%;
diff --git a/web/share/js/kvm/hid.js b/web/share/js/kvm/hid.js
index 67d919cf..ecf8d39f 100644
--- a/web/share/js/kvm/hid.js
+++ b/web/share/js/kvm/hid.js
@@ -30,7 +30,7 @@ import {Keyboard} from "./keyboard.js";
import {Mouse} from "./mouse.js";
-export function Hid(__getResolution, __recorder) {
+export function Hid(__getGeometry, __recorder) {
var self = this;
/************************************************************************/
@@ -40,7 +40,7 @@ export function Hid(__getResolution, __recorder) {
var __init__ = function() {
__keyboard = new Keyboard(__recorder.recordWsEvent);
- __mouse = new Mouse(__getResolution, __recorder.recordWsEvent);
+ __mouse = new Mouse(__getGeometry, __recorder.recordWsEvent);
let hidden_attr = null;
let visibility_change_attr = null;
diff --git a/web/share/js/kvm/mouse.js b/web/share/js/kvm/mouse.js
index d00fae8d..cbcd99da 100644
--- a/web/share/js/kvm/mouse.js
+++ b/web/share/js/kvm/mouse.js
@@ -27,7 +27,7 @@ import {tools, $} from "../tools.js";
import {Keypad} from "../keypad.js";
-export function Mouse(__getResolution, __recordWsEvent) {
+export function Mouse(__getGeometry, __recordWsEvent) {
var self = this;
/************************************************************************/
@@ -227,10 +227,10 @@ export function Mouse(__getResolution, __recordWsEvent) {
if (__absolute) {
let pos = __current_pos;
if (pos.x !== __sent_pos.x || pos.y !== __sent_pos.y) {
- let geo = __getVideoGeometry();
+ let geo = __getGeometry();
let to = {
- "x": __translatePosition(pos.x, geo.x, geo.width, -32768, 32767),
- "y": __translatePosition(pos.y, geo.y, geo.height, -32768, 32767),
+ "x": tools.remap(pos.x, geo.x, geo.width, -32768, 32767),
+ "y": tools.remap(pos.y, geo.y, geo.height, -32768, 32767),
};
tools.debug("Mouse: moved:", 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) {
// https://learn.javascript.ru/mousewheel
// https://stackoverflow.com/a/24595588
diff --git a/web/share/js/kvm/ocr.js b/web/share/js/kvm/ocr.js
new file mode 100644
index 00000000..c7fcf643
--- /dev/null
+++ b/web/share/js/kvm/ocr.js
@@ -0,0 +1,181 @@
+/*****************************************************************************
+# #
+# KVMD - The main PiKVM daemon. #
+# #
+# Copyright (C) 2018-2022 Maxim Devaev <[email protected]> #
+# #
+# 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__();
+}
diff --git a/web/share/js/kvm/session.js b/web/share/js/kvm/session.js
index 81c7c12e..68320655 100644
--- a/web/share/js/kvm/session.js
+++ b/web/share/js/kvm/session.js
@@ -32,6 +32,7 @@ import {Atx} from "./atx.js";
import {Msd} from "./msd.js";
import {Streamer} from "./stream.js";
import {Gpio} from "./gpio.js";
+import {Ocr} from "./ocr.js";
export function Session() {
@@ -46,10 +47,11 @@ export function Session() {
var __streamer = new Streamer();
var __recorder = new Recorder();
- var __hid = new Hid(__streamer.getResolution, __recorder);
+ var __hid = new Hid(__streamer.getGeometry, __recorder);
var __atx = new Atx(__recorder);
var __msd = new Msd();
var __gpio = new Gpio(__recorder);
+ var __ocr = new Ocr(__streamer.getGeometry);
var __init__ = function() {
__startSession();
@@ -251,6 +253,7 @@ export function Session() {
case "atx_state": __atx.setState(data.event); break;
case "msd_state": __msd.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;
}
+ __ocr.setState(null);
__gpio.setState(null);
__hid.setSocket(null);
__recorder.setSocket(null);
diff --git a/web/share/js/kvm/stream.js b/web/share/js/kvm/stream.js
index fad90123..8e68e4a6 100644
--- a/web/share/js/kvm/stream.js
+++ b/web/share/js/kvm/stream.js
@@ -455,8 +455,26 @@ export function Streamer() {
/************************************************************************/
- self.getResolution = function() {
- return __streamer.getResolution();
+ self.getGeometry = function() {
+ // Первоначально обновление геометрии считалось через 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) {
diff --git a/web/share/js/tools.js b/web/share/js/tools.js
index a412cc0d..006453bd 100644
--- a/web/share/js/tools.js
+++ b/web/share/js/tools.js
@@ -83,6 +83,16 @@ export var tools = new function() {
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() {
diff --git a/web/share/js/wm.js b/web/share/js/wm.js
index 78a22140..823272d8 100644
--- a/web/share/js/wm.js
+++ b/web/share/js/wm.js
@@ -84,10 +84,7 @@ function __WindowManager() {
let el_close_button = el_window.querySelector(".window-header .window-button-close");
if (el_close_button) {
el_close_button.title = "Close window";
- tools.el.setOnClick(el_close_button, function() {
- __closeWindow(el_window);
- __activateLastWindow(el_window);
- });
+ tools.el.setOnClick(el_close_button, () => self.closeWindow(el_window));
}
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.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) {
el_window.focus();
el_window.blur();
@@ -460,6 +463,10 @@ function __WindowManager() {
var __makeWindowMovable = function(el_window) {
let el_header = el_window.querySelector(".window-header");
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};