diff options
-rw-r--r-- | keymap.in | 2 | ||||
-rw-r--r-- | kvmd/plugins/hid/otg/__init__.py | 51 | ||||
-rw-r--r-- | kvmd/plugins/hid/otg/device.py | 82 | ||||
-rw-r--r-- | kvmd/plugins/hid/otg/keyboard.py | 18 | ||||
-rw-r--r-- | kvmd/plugins/hid/otg/mouse.py | 7 | ||||
-rw-r--r-- | kvmd/plugins/hid/serial.py | 2 | ||||
-rw-r--r-- | web/kvm/index.html | 36 | ||||
-rw-r--r-- | web/share/css/main.css | 4 | ||||
-rw-r--r-- | web/share/css/menu.css | 8 | ||||
-rw-r--r-- | web/share/js/kvm/keyboard.js | 37 | ||||
-rw-r--r-- | web/share/js/kvm/mouse.js | 10 | ||||
-rw-r--r-- | web/share/svg/led-square.svg | 111 |
12 files changed, 302 insertions, 66 deletions
@@ -21,7 +21,7 @@ # https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code -# https://github.com/NicoHood/HID/blob/master/src/HID-APIs/ImprovedKeylayouts.h +# https://github.com/NicoHood/HID/blob/master/src/KeyboardLayouts/ImprovedKeylayouts.h # https://gist.github.com/MightyPork/6da26e382a7ad91b5496ee55fdc73db2 # ----------------------------------------------------------------------------------- diff --git a/kvmd/plugins/hid/otg/__init__.py b/kvmd/plugins/hid/otg/__init__.py index c5dc1a7a..84c2803e 100644 --- a/kvmd/plugins/hid/otg/__init__.py +++ b/kvmd/plugins/hid/otg/__init__.py @@ -21,6 +21,11 @@ import asyncio +import concurrent.futures +import multiprocessing +import multiprocessing.queues +import queue +import functools from typing import Dict from typing import AsyncGenerator @@ -47,15 +52,12 @@ class Plugin(BaseHid): keyboard: Dict[str, Any], mouse: Dict[str, Any], noop: bool, - state_poll: float, ) -> None: - self.__keyboard_proc = KeyboardProcess(noop=noop, **keyboard) - self.__mouse_proc = MouseProcess(noop=noop, **mouse) + self.__changes_queue: multiprocessing.queues.Queue = multiprocessing.Queue() - self.__state_poll = state_poll - - self.__lock = asyncio.Lock() + self.__keyboard_proc = KeyboardProcess(noop=noop, changes_queue=self.__changes_queue, **keyboard) + self.__mouse_proc = MouseProcess(noop=noop, changes_queue=self.__changes_queue, **mouse) @classmethod def get_plugin_options(cls) -> Dict: @@ -66,16 +68,13 @@ class Plugin(BaseHid): "write_retries": Option(5, type=valid_int_f1), "write_retries_delay": Option(0.1, type=valid_float_f01), }, - "mouse": { "device": Option("", type=valid_abs_path, unpack_as="device_path"), "select_timeout": Option(1.0, type=valid_float_f01), "write_retries": Option(5, type=valid_int_f1), "write_retries_delay": Option(0.1, type=valid_float_f01), }, - - "noop": Option(False, type=valid_bool), - "state_poll": Option(0.1, type=valid_float_f01), + "noop": Option(False, type=valid_bool), } def start(self) -> None: @@ -83,22 +82,30 @@ class Plugin(BaseHid): self.__mouse_proc.start() def get_state(self) -> Dict: - keyboard_online = self.__keyboard_proc.is_online() - mouse_online = self.__mouse_proc.is_online() + keyboard_state = self.__keyboard_proc.get_state() + mouse_state = self.__mouse_proc.get_state() return { - "online": (keyboard_online and mouse_online), - "keyboard": {"online": keyboard_online}, - "mouse": {"online": mouse_online}, + "online": (keyboard_state["online"] and mouse_state["online"]), + "keyboard": {"features": {"leds": True}, **keyboard_state}, + "mouse": mouse_state, } async def poll_state(self) -> AsyncGenerator[Dict, None]: - prev_state: Dict = {} - while self.__keyboard_proc.is_alive() and self.__mouse_proc.is_alive(): - state = self.get_state() - if state != prev_state: - yield self.get_state() - prev_state = state - await asyncio.sleep(self.__state_poll) + loop = asyncio.get_running_loop() + wait_for_changes = functools.partial(self.__changes_queue.get, timeout=1) + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: + prev_state: Dict = {} + while True: + state = self.get_state() + if state != prev_state: + yield state + prev_state = state + while True: + try: + await loop.run_in_executor(executor, wait_for_changes) + break + except queue.Empty: + pass async def reset(self) -> None: self.__keyboard_proc.send_reset_event() diff --git a/kvmd/plugins/hid/otg/device.py b/kvmd/plugins/hid/otg/device.py index 7da6dd18..0fbb63cb 100644 --- a/kvmd/plugins/hid/otg/device.py +++ b/kvmd/plugins/hid/otg/device.py @@ -29,6 +29,9 @@ import queue import errno import time +from typing import Dict +from typing import Any + import setproctitle from ....logging import get_logger @@ -43,6 +46,10 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in def __init__( self, name: str, + read_size: int, + initial_state: Dict, + changes_queue: multiprocessing.queues.Queue, + device_path: str, select_timeout: float, write_retries: int, @@ -53,6 +60,8 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in super().__init__(daemon=True) self.__name = name + self.__read_size = read_size + self.__changes_queue = changes_queue self.__device_path = device_path self.__select_timeout = select_timeout @@ -62,7 +71,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in self.__fd = -1 self.__events_queue: multiprocessing.queues.Queue = multiprocessing.Queue() - self.__online_shared = multiprocessing.Value("i", 1) + self.__state_shared = multiprocessing.Manager().dict(online=True, **initial_state) # type: ignore self.__stop_event = multiprocessing.Event() def run(self) -> None: @@ -75,10 +84,12 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in while not self.__stop_event.is_set(): try: while not self.__stop_event.is_set(): + if self.__ensure_device(): # Check device and process reports if needed + self.__read_all_reports() try: - event: BaseEvent = self.__events_queue.get(timeout=1) + event: BaseEvent = self.__events_queue.get(timeout=0.1) except queue.Empty: - self.__ensure_device() # Check device + pass else: self._process_event(event) except Exception: @@ -89,8 +100,18 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in self.__close_device() - def is_online(self) -> bool: - return bool(self.__online_shared.value and self.is_alive()) + def get_state(self) -> Dict: + return dict(self.__state_shared) + + # ===== + + def _process_event(self, event: BaseEvent) -> None: + raise NotImplementedError + + def _process_read_report(self, report: bytes) -> None: + pass + + # ===== def _stop(self) -> None: if self.is_alive(): @@ -99,9 +120,6 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in if self.exitcode is not None: self.join() - def _process_event(self, event: BaseEvent) -> None: - raise NotImplementedError - def _queue_event(self, event: BaseEvent) -> None: self.__events_queue.put(event) @@ -116,6 +134,11 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in if close: self.__close_device() + def _update_state(self, key: str, value: Any) -> None: + if self.__state_shared[key] != value: + self.__state_shared[key] = value + self.__changes_queue.put(None) + # ===== def __write_report(self, report: bytes) -> bool: @@ -130,7 +153,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in try: written = os.write(self.__fd, report) if written == len(report): - self.__online_shared.value = 1 + self._update_state("online", True) return True else: logger.error("HID-%s write() error: written (%s) != report length (%d)", @@ -151,6 +174,33 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in self.__close_device() return False + def __read_all_reports(self) -> None: + if self.__noop or self.__read_size == 0: + return + + assert self.__fd >= 0 + logger = get_logger() + + read = True + while read: + try: + read = bool(select.select([self.__fd], [], [], 0)[0]) + except Exception as err: + logger.error("Can't select() for read HID-%s: %s: %s", self.__name, type(err).__name__, err) + break + + if read: + try: + report = os.read(self.__fd, self.__read_size) + except Exception as err: + if isinstance(err, OSError) and err.errno == errno.EAGAIN: # pylint: disable=no-member + logger.debug("HID-%s busy/unplugged (read): %s: %s", + self.__name, type(err).__name__, err) + else: + logger.exception("Can't read report from HID-%s", self.__name) + else: + self._process_read_report(report) + def __ensure_device(self) -> bool: if self.__noop: return True @@ -159,25 +209,29 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in if self.__fd < 0: try: - self.__fd = os.open(self.__device_path, os.O_WRONLY|os.O_NONBLOCK) + flags = os.O_NONBLOCK + flags |= (os.O_RDWR if self.__read_size else os.O_WRONLY) + self.__fd = os.open(self.__device_path, flags) except FileNotFoundError: logger.error("Missing HID-%s device: %s", self.__name, self.__device_path) + time.sleep(self.__select_timeout) except Exception as err: logger.error("Can't open HID-%s device: %s: %s: %s", self.__name, self.__device_path, type(err).__name__, err) + time.sleep(self.__select_timeout) if self.__fd >= 0: try: if select.select([], [self.__fd], [], self.__select_timeout)[1]: - self.__online_shared.value = 1 + self._update_state("online", True) return True else: - logger.debug("HID-%s is busy/unplugged (select)", self.__name) + logger.debug("HID-%s is busy/unplugged (write select)", self.__name) except Exception as err: - logger.error("Can't select() HID-%s: %s: %s", self.__name, type(err).__name__, err) + logger.error("Can't select() for write HID-%s: %s: %s", self.__name, type(err).__name__, err) self.__close_device() - self.__online_shared.value = 0 + self._update_state("online", False) return False def __close_device(self) -> None: diff --git a/kvmd/plugins/hid/otg/keyboard.py b/kvmd/plugins/hid/otg/keyboard.py index 6cf8f72d..6c9ddfed 100644 --- a/kvmd/plugins/hid/otg/keyboard.py +++ b/kvmd/plugins/hid/otg/keyboard.py @@ -65,7 +65,12 @@ class _KeyEvent(BaseEvent): # ===== class KeyboardProcess(BaseDeviceProcess): def __init__(self, **kwargs: Any) -> None: - super().__init__(name="keyboard", **kwargs) + super().__init__( + name="keyboard", + read_size=1, + initial_state={"leds": {"caps": False, "scroll": False, "num": False}}, + **kwargs, + ) self.__pressed_modifiers: Set[keymap.OtgKey] = set() self.__pressed_keys: List[Optional[keymap.OtgKey]] = [None] * 6 @@ -90,6 +95,17 @@ class KeyboardProcess(BaseDeviceProcess): # ===== + def _process_read_report(self, report: bytes) -> None: + # https://wiki.osdev.org/USB_Human_Interface_Devices#LED_lamps + assert len(report) == 1, report + self._update_state("leds", { + "caps": bool(report[0] & 2), + "scroll": bool(report[0] & 4), + "num": bool(report[0] & 1), + }) + + # ===== + def _process_event(self, event: BaseEvent) -> None: if isinstance(event, _ClearEvent): self.__process_clear_event() diff --git a/kvmd/plugins/hid/otg/mouse.py b/kvmd/plugins/hid/otg/mouse.py index 172ed666..a31d7ae5 100644 --- a/kvmd/plugins/hid/otg/mouse.py +++ b/kvmd/plugins/hid/otg/mouse.py @@ -61,7 +61,12 @@ class _WheelEvent(BaseEvent): # ===== class MouseProcess(BaseDeviceProcess): def __init__(self, **kwargs: Any) -> None: - super().__init__(name="mouse", **kwargs) + super().__init__( + name="mouse", + read_size=0, + initial_state={}, + **kwargs, + ) self.__pressed_buttons: int = 0 self.__x = 0 diff --git a/kvmd/plugins/hid/serial.py b/kvmd/plugins/hid/serial.py index 9927b3aa..d5561e2f 100644 --- a/kvmd/plugins/hid/serial.py +++ b/kvmd/plugins/hid/serial.py @@ -191,7 +191,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst online = bool(self.__online_shared.value) return { "online": online, - "keyboard": {"online": online}, + "keyboard": {"features": {"leds": False}, "online": online}, "mouse": {"online": online}, } diff --git a/web/kvm/index.html b/web/kvm/index.html index 5411f43f..8a4f759b 100644 --- a/web/kvm/index.html +++ b/web/kvm/index.html @@ -158,7 +158,7 @@ <div class="menu-item-content-text"> <table> <tr> - <td><img src="../share/svg/warning.svg" /></td> + <td><img class="sign" src="../share/svg/warning.svg" /></td> <td><b>Mass Storage Device is offline</b></td> </tr> </table> @@ -169,7 +169,7 @@ <div class="menu-item-content-text"> <table> <tr> - <td><img src="../share/svg/warning.svg" /></td> + <td><img class="sign" src="../share/svg/warning.svg" /></td> <td><b>Current image is broken!</b><br><sub>Perhaps uploading was interrupted</sub></td> </tr> </table> @@ -180,7 +180,7 @@ <div class="menu-item-content-text"> <table> <tr> - <td><img src="../share/svg/warning.svg" /></td> + <td><img class="sign" src="../share/svg/warning.svg" /></td> <td><b>Current image is too big for CD-ROM!</b><br><sub>The device filesystem will be truncated to 2.2GiB</sub></td> </tr> </table> @@ -191,7 +191,7 @@ <div class="menu-item-content-text"> <table> <tr> - <td><img src="../share/svg/info.svg" /></td> + <td><img class="sign" src="../share/svg/info.svg" /></td> <td><b>Current image is out of storage</b><br><sub>This image was connected manually using <b>kvmd-otgmsd</b></sub></td> </tr> </table> @@ -202,7 +202,7 @@ <div class="menu-item-content-text"> <table> <tr> - <td><img src="../share/svg/info.svg" /></td> + <td><img class="sign" src="../share/svg/info.svg" /></td> <td><b>Another user uploads an image</b></td> </tr> </table> @@ -311,7 +311,11 @@ <button disabled data-force-hide-menu id="hid-pak-button">• ↳ Paste-as-Keys <sup><i>ascii-only</i></sup></button> <hr> <div class="buttons-row"> - <button data-force-hide-menu data-shortcut="CapsLock" class="row50">• CapsLock</button> + <button data-force-hide-menu data-shortcut="CapsLock" class="row50"> + • + <img class="inline hid-keyboard-leds hid-keyboard-caps-led led-gray feature-disabled" src="../share/svg/led-square.svg" /> + CapsLock + </button> <button data-force-hide-menu data-shortcut="MetaLeft" class="row50">• Left Win</button> </div> <hr> @@ -427,7 +431,10 @@ <div data-code="Backslash" class="key"><p>|<br>\</p></div> </div> <div class="keypad-row"> - <div data-code="CapsLock" class="key wide-3 left small"><p>Caps Lock</p></div> + <div data-code="CapsLock" class="key wide-3 left small"> + <img class="inline hid-keyboard-leds hid-keyboard-caps-led led-gray feature-disabled" src="../share/svg/led-square.svg" /> + <p>Caps Lock</p> + </div> <div data-code="KeyA" class="key single"><p>A</p></div> <div data-code="KeyS" class="key single"><p>S</p></div> <div data-code="KeyD" class="key single"><p>D</p></div> @@ -468,7 +475,10 @@ <div class="keypad-block"> <div class="keypad-row"> <div data-code="PrintScreen" class="modifier small"><p><b>•</b><br>Pt/Sq</p></div> - <div data-code="ScrollLock" class="key small"><p>ScrLk</p></div> + <div data-code="ScrollLock" class="key small"> + <img class="inline hid-keyboard-leds hid-keyboard-scroll-led led-gray feature-disabled" src="../share/svg/led-square.svg" /> + <p>ScrLk</p> + </div> <div data-code="Pause" class="key small"><p>P/Brk</p></div> </div> <hr> @@ -515,7 +525,10 @@ <div data-code="F12" class="key wide-0 margin-0 small"><p>F12</p></div> <div class="empty-key" style="width:5px"></div> <div data-code="PrintScreen" class="modifier margin-0 small"><p><b>•</b><br>Pt/Sq</p></div> - <div data-code="ScrollLock" class="key margin-0 small"><p>ScrLk</p></div> + <div data-code="ScrollLock" class="key margin-0 small"> + <img class="inline hid-keyboard-leds hid-keyboard-scroll-led led-gray feature-disabled" src="../share/svg/led-square.svg" /> + <p>ScrLk</p> + </div> <div data-code="Pause" class="key margin-0 small"><p>P/Brk</p></div> <div data-code="Insert" class="key margin-0 small"><p>Ins</p></div> <div data-code="Home" class="key margin-0 small"><p>Home</p></div> @@ -556,7 +569,10 @@ <div data-code="Backslash" class="key wide-2 left" style="width:78px"><p>|<br>\</p></div> </div> <div class="keypad-row"> - <div data-code="CapsLock" class="key wide-3 left small"><p>Caps Lock</p></div> + <div data-code="CapsLock" class="key wide-3 left small"> + <img class="inline hid-keyboard-leds hid-keyboard-caps-led led-gray feature-disabled" src="../share/svg/led-square.svg" /> + <p>Caps Lock</p> + </div> <div data-code="KeyA" class="key single"><p>A</p></div> <div data-code="KeyS" class="key single"><p>S</p></div> <div data-code="KeyD" class="key single"><p>D</p></div> diff --git a/web/share/css/main.css b/web/share/css/main.css index c97b103d..ab506a1b 100644 --- a/web/share/css/main.css +++ b/web/share/css/main.css @@ -80,6 +80,10 @@ img.svg-gray { filter: invert(0.7); vertical-align: middle; } +img.inline { + vertical-align: middle; + height: 0.8em; +} button, select { diff --git a/web/share/css/menu.css b/web/share/css/menu.css index 5a7b9d22..a4babb73 100644 --- a/web/share/css/menu.css +++ b/web/share/css/menu.css @@ -53,7 +53,7 @@ ul#menu li a#menu-logo { text-decoration: none; } -ul#menu img { +ul#menu li a.menu-item img { vertical-align: middle; margin-right: 10px; height: 20px; @@ -150,3 +150,9 @@ ul#menu li div.menu-item-content hr { border: none; border-top: var(--border-control-thin); } + +ul#menu li div.menu-item-content img.sign { + vertical-align: middle; + margin-right: 10px; + height: 20px; +} diff --git a/web/share/js/kvm/keyboard.js b/web/share/js/kvm/keyboard.js index a443b2c9..0398baef 100644 --- a/web/share/js/kvm/keyboard.js +++ b/web/share/js/kvm/keyboard.js @@ -20,7 +20,7 @@ *****************************************************************************/ -import {tools, $} from "../tools.js"; +import {tools, $, $$$} from "../tools.js"; import {Keypad} from "../keypad.js"; @@ -42,16 +42,16 @@ export function Keyboard() { $("keyboard-window").onkeydown = (event) => __keyboardHandler(event, true); $("keyboard-window").onkeyup = (event) => __keyboardHandler(event, false); - $("keyboard-window").onfocus = __updateLeds; - $("keyboard-window").onblur = __updateLeds; + $("keyboard-window").onfocus = __updateOnlineLeds; + $("keyboard-window").onblur = __updateOnlineLeds; $("stream-window").onkeydown = (event) => __keyboardHandler(event, true); $("stream-window").onkeyup = (event) => __keyboardHandler(event, false); - $("stream-window").onfocus = __updateLeds; - $("stream-window").onblur = __updateLeds; + $("stream-window").onfocus = __updateOnlineLeds; + $("stream-window").onblur = __updateOnlineLeds; - window.addEventListener("focusin", __updateLeds); - window.addEventListener("focusout", __updateLeds); + window.addEventListener("focusin", __updateOnlineLeds); + window.addEventListener("focusout", __updateOnlineLeds); if (tools.browser.is_mac) { // https://bugs.chromium.org/p/chromium/issues/detail?id=28089 @@ -68,12 +68,29 @@ export function Keyboard() { self.releaseAll(); __ws = ws; } - __updateLeds(); + __updateOnlineLeds(); }; self.setState = function(state) { __online = state.online; - __updateLeds(); + __updateOnlineLeds(); + + for (let el of $$$(".hid-keyboard-leds")) { + console.log(el, state.features.leds); + el.classList.toggle("feature-disabled", !state.features.leds); + } + + for (let led of ["caps", "scroll", "num"]) { + for (let el of $$$(`.hid-keyboard-${led}-led`)) { + if (state.leds[led]) { + el.classList.add("led-green"); + el.classList.remove("led-gray"); + } else { + el.classList.add("led-gray"); + el.classList.remove("led-green"); + } + } + } }; self.releaseAll = function() { @@ -84,7 +101,7 @@ export function Keyboard() { __keyboardHandler({code: code}, state); }; - var __updateLeds = function() { + var __updateOnlineLeds = function() { let is_captured = ( $("stream-window").classList.contains("window-active") || $("keyboard-window").classList.contains("window-active") diff --git a/web/share/js/kvm/mouse.js b/web/share/js/kvm/mouse.js index 1b8aa716..b4978dfe 100644 --- a/web/share/js/kvm/mouse.js +++ b/web/share/js/kvm/mouse.js @@ -65,12 +65,12 @@ export function Mouse() { self.setSocket = function(ws) { __ws = ws; $("stream-box").classList.toggle("stream-box-mouse-enabled", ws); - __updateLeds(); + __updateOnlineLeds(); }; self.setState = function(state) { __online = state.online; - __updateLeds(); + __updateOnlineLeds(); }; self.releaseAll = function() { @@ -79,15 +79,15 @@ export function Mouse() { var __hoverStream = function() { __stream_hovered = true; - __updateLeds(); + __updateOnlineLeds(); }; var __leaveStream = function() { __stream_hovered = false; - __updateLeds(); + __updateOnlineLeds(); }; - var __updateLeds = function() { + var __updateOnlineLeds = function() { let is_captured = (__stream_hovered || tools.browser.is_ios); let led = "led-gray"; let title = "Mouse free"; diff --git a/web/share/svg/led-square.svg b/web/share/svg/led-square.svg new file mode 100644 index 00000000..f677e004 --- /dev/null +++ b/web/share/svg/led-square.svg @@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + id="Layer_1" + x="0px" + y="0px" + viewBox="0 0 512 512" + style="enable-background:new 0 0 512 512;" + xml:space="preserve" + sodipodi:docname="led-square.svg" + inkscape:version="0.92.4 5da689c313, 2019-01-14"><metadata + id="metadata85"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs + id="defs83"> + + + + </defs><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1920" + inkscape:window-height="1020" + id="namedview81" + showgrid="false" + inkscape:zoom="0.921875" + inkscape:cx="223.44119" + inkscape:cy="239.43047" + inkscape:window-x="0" + inkscape:window-y="30" + inkscape:window-maximized="1" + inkscape:current-layer="Layer_1" /> + + + +<path + d="M 460,0 H 52 C 23.28,0 0,23.28 0,52 v 408 c 0,28.72 23.28,52 52,52 h 408 c 28.72,0 52,-23.28 52,-52 V 52 C 512,23.28 488.72,0 460,0 Z" + id="path20" + inkscape:connector-curvature="0" + sodipodi:nodetypes="sssssssss" /> + + + + +<g + id="g50"> +</g> +<g + id="g52"> +</g> +<g + id="g54"> +</g> +<g + id="g56"> +</g> +<g + id="g58"> +</g> +<g + id="g60"> +</g> +<g + id="g62"> +</g> +<g + id="g64"> +</g> +<g + id="g66"> +</g> +<g + id="g68"> +</g> +<g + id="g70"> +</g> +<g + id="g72"> +</g> +<g + id="g74"> +</g> +<g + id="g76"> +</g> +<g + id="g78"> +</g> +<rect + style="opacity:1;fill:none;fill-opacity:1;paint-order:normal" + id="rect4830" + width="478.37289" + height="351.45764" + x="26.033897" + y="58.576271" /></svg>
\ No newline at end of file |