This commit is contained in:
Maxim Devaev 2022-02-21 04:18:15 +03:00
parent 67839a52a2
commit 96191a1b08
18 changed files with 376 additions and 106 deletions

View File

@ -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": {

View File

@ -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,

View File

@ -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,

View File

@ -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)

View File

@ -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">&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> </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>

View File

@ -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)

42
web/kvm/navbar-text.pug Normal file
View 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") &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

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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%;

View File

@ -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;

View File

@ -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
View 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__();
}

View File

@ -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);

View File

@ -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) {

View File

@ -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() {

View File

@ -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};