diff --git a/GUI/IPconvGUI.py b/GUI/IPconvGUI.py new file mode 100644 index 0000000..d4cf050 --- /dev/null +++ b/GUI/IPconvGUI.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +# Copyright (C) 2014, 2018 Brandon M. Pace +# +# This file is part of Quick IP Converter +# +# Quick IP Converter is free software: you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Quick IP Converter 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with Quick IP Converter. +# If not, see . + + +import wx +import wx.adv +import wx.lib.agw.persist as pm +from .IPconvGUIbase import BaseConverterFrame, BaseSettingsFrame +from .resources import * + + +# ctrl-[A,C,V,X,Z], shift-insert +ctrl_ascii_chars = [ + wx.WXK_CONTROL_A, wx.WXK_CONTROL_C, wx.WXK_CONTROL_V, wx.WXK_CONTROL_X, wx.WXK_CONTROL_Z, wx.WXK_INSERT +] + + +class IPConverterFrame(BaseConverterFrame): + def __init__(self, *args, **kwds): + BaseConverterFrame.__init__(self, *args, **kwds) + # Used for dragging implementation + self._initial_position = None + + # Last selected text control is remembered for use with reverse checkbox callback + self.last_selected = self.text_ctrl_hex + + self.settings_window = SettingsFrame(self, name='SettingsFrame') + + self.themes = { + 'dark': { + 'background': {'main': wx.Colour(60, 60, 60), 'text': wx.Colour(85, 85, 85)}, + 'foreground': {'main': wx.Colour(192, 192, 192), 'text': wx.Colour(232, 232, 232)} + }, + 'light': { + 'background': {'main': wx.Colour(238, 238, 238), 'text': wx.Colour(255, 255, 255)}, + 'foreground': {'main': wx.Colour(0, 0, 0), 'text': wx.Colour(0, 0, 0)} + } + } + + # Hotkey registration and binding + # TODO: Check the return from RegisterHotKey + # TODO: Identify why Win+Z wasn't working for me on Windows 10 (Ctrl-Win-Z works) + self.RegisterHotKey(self.text_ctrl_hex.GetId(), wx.MOD_CONTROL | wx.MOD_META, ord('Z')) # Ctrl-Win-Z + self.Bind(wx.EVT_HOTKEY, self.on_paste, id=self.text_ctrl_hex.GetId()) + + # Catching when the window transitions to/from being the active window the user is interacting with + self.Bind(wx.EVT_ACTIVATE, self.on_activate) + + # Allow general handling of focus events for any relevant widget + self.Bind(wx.EVT_CHILD_FOCUS, self.on_child_focus) + + # Allow 'minimizing' the window when pressing Escape if the frame has focus + self.panel_main.Bind(wx.EVT_CHAR_HOOK, self.on_key) + + # Mouse bindings (for dragging the panel) + self.panel_main.Bind(wx.EVT_LEFT_UP, self.on_mouse_button_up) + self.panel_main.Bind(wx.EVT_MOTION, self.on_mouse) + self.panel_main.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.on_mouse_lost) + + # Catching text input, used to automatically run conversions + self.Bind(wx.EVT_TEXT, self.on_text, self.text_ctrl_dec) + self.Bind(wx.EVT_TEXT, self.on_text, self.text_ctrl_dotted) + self.Bind(wx.EVT_TEXT, self.on_text, self.text_ctrl_hex) + + # Catching individual character input, to only allow valid characters + self.text_ctrl_dec.Bind(wx.EVT_CHAR, self.on_char) + self.text_ctrl_dotted.Bind(wx.EVT_CHAR, self.on_char) + self.text_ctrl_hex.Bind(wx.EVT_CHAR, self.on_char) + + self.text_ctrl_dec.Bind(wx.EVT_TEXT_PASTE, self.on_paste) + self.text_ctrl_dotted.Bind(wx.EVT_TEXT_PASTE, self.on_paste) + self.text_ctrl_hex.Bind(wx.EVT_TEXT_PASTE, self.on_paste) + + self.SetName('MainFrame') + self.checkbox_reverse.SetName('checkbox_reverse') + + # TODO: Confirm behavior with multiple monitors, especially on non-Windows systems. + self.Center() + + self.persistence_manager = pm.PersistenceManager.Get() + self.persistence_manager.SetManagerStyle(pm.PM_DEFAULT_STYLE | pm.PM_PERSIST_CONTROL_VALUE) + + # Restore any saved selections and window placement + self.persistence_manager.RegisterAndRestore(self) + self.persistence_manager.RegisterAndRestoreAll( + self, children=[ + self.checkbox_reverse, self.settings_window.checkbox_monitorclipboard, + self.settings_window.checkbox_stayontop, self.settings_window.radio_box_theme + ] + ) + + self.apply_theme(theme_name=self.settings_window.radio_box_theme.GetStringSelection()) + self.monitor_clipboard_start() + self.stay_on_top(enable=self.settings_window.checkbox_stayontop.IsChecked()) + + def apply_theme(self, theme_name: str): + theme = self.themes.get(theme_name, None) + if not theme: + return # TODO: consider raising an exception, especially if customization is introduced. + + # Main colors + background_main = theme['background']['main'] + foreground_main = theme['foreground']['main'] + + # Colors for TextCtrl objects + background_text = theme['background']['text'] + foreground_text = theme['foreground']['text'] + + self.checkbox_reverse.SetBackgroundColour(background_main) + self.checkbox_reverse.SetForegroundColour(foreground_main) + + self.bitmap_button_settings.SetBackgroundColour(background_main) + self.bitmap_button_exit.SetBackgroundColour(background_main) + + self.text_ctrl_dotted.SetBackgroundColour(background_text) + self.text_ctrl_dotted.SetForegroundColour(foreground_text) + self.text_ctrl_hex.SetBackgroundColour(background_text) + self.text_ctrl_hex.SetForegroundColour(foreground_text) + self.text_ctrl_dec.SetBackgroundColour(background_text) + self.text_ctrl_dec.SetForegroundColour(foreground_text) + + self.label_dotted.SetBackgroundColour(background_main) + self.label_dotted.SetForegroundColour(foreground_main) + self.label_hex.SetBackgroundColour(background_main) + self.label_hex.SetForegroundColour(foreground_main) + self.label_dec.SetBackgroundColour(background_main) + self.label_dec.SetForegroundColour(foreground_main) + + self.panel_main.SetBackgroundColour(background_main) + self.panel_main.SetForegroundColour(foreground_main) + + self.Refresh() + + @staticmethod + def get_clipboard_string() -> tuple: + """Try to get text data from the clipboard and return a tuple indicating success and the text value""" + clipboard_string = '' + success = False + text_data = wx.TextDataObject() + if wx.TheClipboard.Open(): + success = wx.TheClipboard.GetData(text_data) + wx.TheClipboard.Close() + if success: + clipboard_string = text_data.GetText() + return success, clipboard_string + + def monitor_clipboard(self): + """Monitor the clipboard when this application does not have focus""" + if self.settings_window.checkbox_monitorclipboard.IsChecked() and (not self.IsActive()): + # Submit another run to start later + self.monitor_clipboard_start() + # Return the data, which should be used in a subclass + return self.get_clipboard_string() + else: + return None, None + + def monitor_clipboard_start(self): + """Submit a delayed call for monitoring. This is useful to allow event handlers to complete beforehand.""" + wx.CallLater(1000, self.monitor_clipboard) + + def on_activate(self, event): + """This is triggered when the user changes focus between this application and others""" + # Check for valid self in case window has been destroyed (avoids exception when closing the program) + if self and self.settings_window: + # Start monitoring clipboard if we're configured to and another application has focus + if self.settings_window.checkbox_monitorclipboard.IsChecked() and (not event.GetActive()): + self.monitor_clipboard_start() + event.Skip() + + def on_button_exit(self, event): + """Save window position and settings when the user presses the button to close the application""" + self.persistence_manager.SaveAndUnregister() + self.Close(force=True) + + def on_button_settings(self, event): + """Open the settings window, or focus it if it's already open.""" + if self.settings_window.IsShown(): + self.settings_window.Raise() + else: + self.settings_window.Show() + self.settings_window.CenterOnParent() + self.settings_window.checkbox_monitorclipboard.SetFocus() + + def on_char(self, event): + """Allow movement and command keys in TextCtrl objects""" + if event.IsKeyInCategory(wx.WXK_CATEGORY_NAVIGATION | wx.WXK_CATEGORY_CUT | wx.WXK_CATEGORY_TAB): + event.Skip() + elif event.GetKeyCode() in ctrl_ascii_chars: + event.Skip() + + def on_checkbox_reverse(self, event): + print("Event handler 'on_checkbox_reverse' not implemented!") + event.Skip() + + def on_child_focus(self, event): + """Allows general handling of focus events for different types of widgets throughout the program""" + event_object = event.GetEventObject() + if isinstance(event_object, wx.TextCtrl): + if event_object in [self.text_ctrl_dotted, self.text_ctrl_hex, self.text_ctrl_dec]: + self.last_selected = event_object + wx.CallAfter(event_object.SetInsertionPointEnd) + wx.CallAfter(event_object.SelectAll) + else: + event.Skip() + + def on_key(self, event): + """Minimize (iconize) the window on escape key""" + if event.GetKeyCode() == wx.WXK_ESCAPE: + self.Iconize() + else: + event.Skip() + + def on_mouse(self, event): + """Implement click-and-drag for the frame/panel""" + if not event.Dragging(): + # Panel is not being dragged, reset and bail + self._initial_position = None + return + if not self.panel_main.HasCapture(): + # In Drag event, make sure we capture the mouse + self.panel_main.CaptureMouse() + if not self._initial_position: + # Panel is being dragged, store current position + self._initial_position = event.GetPosition() + else: + # Panel is being dragged and we already have a previous position, move the window + new_position = event.GetPosition() + delta = self._initial_position - new_position + self.SetPosition(self.GetPosition() - delta) + + def on_mouse_button_up(self, event): + """Makes sure the mouse gets released from dragging""" + if self.panel_main.HasCapture(): + self.panel_main.ReleaseMouse() + + def on_mouse_lost(self, event): + """This function can be used to abort anything relying on mouse input.""" + pass + + def on_paste(self, event) -> tuple: + """This will get the value from the clipboard and return a success indicator and string in a tuple""" + return self.get_clipboard_string() + + def on_text(self, event): + print("Event handler 'on_text' not implemented!") + event.Skip() + + def stay_on_top(self, enable: bool = True): + if enable: + # Binary OR wx.STAY_ON_TOP to add it if it's not already present + self.SetWindowStyle(self.GetWindowStyle() | wx.STAY_ON_TOP) + else: + # Binary XOR wx.STAY_ON_TOP to remove it if it is present + self.SetWindowStyle(self.GetWindowStyle() ^ wx.STAY_ON_TOP) + + +class SettingsFrame(BaseSettingsFrame): + def __init__(self, *args, **kwds): + kwds["style"] = kwds.get("style", 0) + BaseSettingsFrame.__init__(self, *args, **kwds) + self.Bind(wx.EVT_CLOSE, self.on_close) + + self.checkbox_monitorclipboard.SetName('checkbox_monitorclipboard') + self.checkbox_stayontop.SetName('checkbox_stayontop') + self.radio_box_theme.SetName('radio_box_theme') + + def on_about(self, event): + program_description = \ + """Quick IP Converter can convert IPv4 addresses between dotted-quad, decimal and hexadecimal notation.""" + program_license = \ + "LGPL-3.0-or-later (GNU Lesser General Public License 3.0 or later)\n" \ + "See the files COPYING and COPYING.LESSER distributed with this program\n" \ + "or https://www.gnu.org/licenses/ if you did not receive them with your copy." + info = wx.adv.AboutDialogInfo() + info.SetIcon(IPconvPNG.GetIcon()) + info.SetName('Quick IP Converter') + info.SetVersion('2.0') + info.SetDescription(program_description) + info.SetCopyright('(C) 2014, 2018 Brandon M. Pace ') + info.SetWebSite('https://github.com/brandonmpace/Quick-IP-Converter') + info.SetLicense(program_license) + # info.AddDeveloper('Brandon M. Pace') + + wx.adv.AboutBox(info, parent=self) + + def on_checkbox_monitorclipboard(self, event): + if event.IsChecked(): + self.Parent.monitor_clipboard_start() + event.Skip() + + def on_checkbox_stayontop(self, event): + self.Parent.stay_on_top(event.IsChecked()) + event.Skip() + + def on_close(self, event): + """This function is used to just hide the settings window when the user clicks the close button.""" + if event.CanVeto(): + self.Hide() + event.Veto() + else: + event.Skip() + + def on_radiobox_theme(self, event): + self.Parent.apply_theme(event.GetString()) + event.Skip() diff --git a/GUI/IPconvGUIbase.py b/GUI/IPconvGUIbase.py new file mode 100644 index 0000000..6aba2f8 --- /dev/null +++ b/GUI/IPconvGUIbase.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# generated by wxGlade 0.9.0b1 on Sun Dec 16 21:09:45 2018 +# + +import wx +import wx.adv + +# begin wxGlade: dependencies +# end wxGlade + +# begin wxGlade: extracode +from .resources import * + +# Copyright (C) 2014, 2018 Brandon M. Pace +# +# This file is part of Quick IP Converter +# +# Quick IP Converter is free software: you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Quick IP Converter 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with Quick IP Converter. +# If not, see . +# end wxGlade + + +class BaseConverterFrame(wx.Frame): + def __init__(self, *args, **kwds): + # begin wxGlade: BaseConverterFrame.__init__ + kwds["style"] = kwds.get("style", 0) | wx.CLIP_CHILDREN | wx.STAY_ON_TOP | wx.TAB_TRAVERSAL + wx.Frame.__init__(self, *args, **kwds) + self.panel_main = wx.Panel(self, wx.ID_ANY) + self.checkbox_reverse = wx.CheckBox(self.panel_main, wx.ID_ANY, "Reverse", style=wx.ALIGN_RIGHT) + self.bitmap_button_settings = wx.BitmapButton(self.panel_main, wx.ID_ANY, SettingsIcon.GetBitmap(), style=wx.BORDER_NONE | wx.BU_AUTODRAW | wx.BU_EXACTFIT | wx.BU_NOTEXT) + self.bitmap_button_exit = wx.BitmapButton(self.panel_main, wx.ID_ANY, ExitIcon.GetBitmap(), style=wx.BORDER_NONE | wx.BU_AUTODRAW | wx.BU_EXACTFIT | wx.BU_NOTEXT) + self.label_dotted = wx.StaticText(self.panel_main, wx.ID_ANY, "IP:", style=wx.ALIGN_RIGHT) + self.text_ctrl_dotted = wx.TextCtrl(self.panel_main, wx.ID_ANY, "") + self.label_hex = wx.StaticText(self.panel_main, wx.ID_ANY, "Hex:", style=wx.ALIGN_RIGHT) + self.text_ctrl_hex = wx.TextCtrl(self.panel_main, wx.ID_ANY, "") + self.label_dec = wx.StaticText(self.panel_main, wx.ID_ANY, "Dec:") + self.text_ctrl_dec = wx.TextCtrl(self.panel_main, wx.ID_ANY, "") + + self.__set_properties() + self.__do_layout() + + self.Bind(wx.EVT_CHECKBOX, self.on_checkbox_reverse, self.checkbox_reverse) + self.Bind(wx.EVT_BUTTON, self.on_button_settings, self.bitmap_button_settings) + self.Bind(wx.EVT_BUTTON, self.on_button_exit, self.bitmap_button_exit) + # end wxGlade + + def __set_properties(self): + # begin wxGlade: BaseConverterFrame.__set_properties + self.SetTitle("Quick IP Converter") + _icon = wx.NullIcon + _icon.CopyFromBitmap(IPconvIcon.GetBitmap()) + self.SetIcon(_icon) + self.checkbox_reverse.SetBackgroundColour(wx.Colour(238, 238, 238)) + self.checkbox_reverse.SetForegroundColour(wx.Colour(0, 0, 0)) + self.checkbox_reverse.SetToolTip("Reverse the byte order during conversion") + self.bitmap_button_settings.SetBackgroundColour(wx.Colour(238, 238, 238)) + self.bitmap_button_settings.SetToolTip("Settings") + self.bitmap_button_settings.SetSize(self.bitmap_button_settings.GetBestSize()) + self.bitmap_button_exit.SetBackgroundColour(wx.Colour(238, 238, 238)) + self.bitmap_button_exit.SetToolTip("Exit") + self.bitmap_button_exit.SetSize(self.bitmap_button_exit.GetBestSize()) + self.label_dotted.SetBackgroundColour(wx.Colour(238, 238, 238)) + self.label_dotted.SetForegroundColour(wx.Colour(0, 0, 0)) + self.text_ctrl_dotted.SetBackgroundColour(wx.Colour(255, 255, 255)) + self.text_ctrl_dotted.SetForegroundColour(wx.Colour(0, 0, 0)) + self.text_ctrl_dotted.SetToolTip("Dotted-quad IPv4 (e.g. 192.168.1.1)") + self.label_hex.SetBackgroundColour(wx.Colour(238, 238, 238)) + self.label_hex.SetForegroundColour(wx.Colour(0, 0, 0)) + self.text_ctrl_hex.SetBackgroundColour(wx.Colour(255, 255, 255)) + self.text_ctrl_hex.SetForegroundColour(wx.Colour(0, 0, 0)) + self.text_ctrl_hex.SetToolTip("Hexadecimal IP (e.g. c0a80101) or hexadecimal value") + self.text_ctrl_hex.SetFocus() + self.label_dec.SetBackgroundColour(wx.Colour(238, 238, 238)) + self.label_dec.SetForegroundColour(wx.Colour(0, 0, 0)) + self.text_ctrl_dec.SetBackgroundColour(wx.Colour(255, 255, 255)) + self.text_ctrl_dec.SetForegroundColour(wx.Colour(0, 0, 0)) + self.text_ctrl_dec.SetToolTip("Decimal IP (e.g. 3232235777) or decimal value") + self.panel_main.SetBackgroundColour(wx.Colour(238, 238, 238)) + self.panel_main.SetForegroundColour(wx.Colour(0, 0, 0)) + self.panel_main.SetToolTip("Click and drag to move the window") + # end wxGlade + + def __do_layout(self): + # begin wxGlade: BaseConverterFrame.__do_layout + sizer_main_outer = wx.BoxSizer(wx.VERTICAL) + grid_sizer_main = wx.GridBagSizer(0, 0) + sizer_buttons = wx.BoxSizer(wx.HORIZONTAL) + grid_sizer_main.Add(self.checkbox_reverse, (0, 0), (1, 2), wx.ALIGN_CENTER_VERTICAL, 0) + sizer_buttons.Add(self.bitmap_button_settings, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT, 0) + sizer_buttons.Add(self.bitmap_button_exit, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT | wx.EXPAND | wx.LEFT, 2) + grid_sizer_main.Add(sizer_buttons, (0, 3), (1, 1), wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT | wx.EXPAND, 0) + grid_sizer_main.Add(self.label_dotted, (1, 0), (1, 1), wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT, 0) + grid_sizer_main.Add(self.text_ctrl_dotted, (1, 1), (1, 3), wx.EXPAND, 0) + grid_sizer_main.Add(self.label_hex, (2, 0), (1, 1), wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT, 0) + grid_sizer_main.Add(self.text_ctrl_hex, (2, 1), (1, 3), wx.EXPAND, 0) + grid_sizer_main.Add(self.label_dec, (3, 0), (1, 1), wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT, 0) + grid_sizer_main.Add(self.text_ctrl_dec, (3, 1), (1, 3), wx.EXPAND, 0) + self.panel_main.SetSizer(grid_sizer_main) + grid_sizer_main.AddGrowableCol(1) + sizer_main_outer.Add(self.panel_main, 1, wx.EXPAND, 0) + self.SetSizer(sizer_main_outer) + sizer_main_outer.Fit(self) + self.Layout() + # end wxGlade + + def on_checkbox_reverse(self, event): # wxGlade: BaseConverterFrame. + print("Event handler 'on_checkbox_reverse' not implemented!") + event.Skip() + + def on_button_settings(self, event): # wxGlade: BaseConverterFrame. + print("Event handler 'on_button_settings' not implemented!") + event.Skip() + + def on_button_exit(self, event): # wxGlade: BaseConverterFrame. + print("Event handler 'on_button_exit' not implemented!") + event.Skip() + +# end of class BaseConverterFrame + +class BaseSettingsFrame(wx.Frame): + def __init__(self, *args, **kwds): + # begin wxGlade: BaseSettingsFrame.__init__ + kwds["style"] = kwds.get("style", 0) | wx.CAPTION | wx.CLIP_CHILDREN | wx.CLOSE_BOX | wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_TOOL_WINDOW | wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX | wx.SYSTEM_MENU | wx.TAB_TRAVERSAL + wx.Frame.__init__(self, *args, **kwds) + self.panel_settings = wx.Panel(self, wx.ID_ANY) + self.checkbox_monitorclipboard = wx.CheckBox(self.panel_settings, wx.ID_ANY, "monitor clipboard") + self.checkbox_stayontop = wx.CheckBox(self.panel_settings, wx.ID_ANY, "stay on top") + self.radio_box_theme = wx.RadioBox(self.panel_settings, wx.ID_ANY, "theme", choices=["dark", "light"], majorDimension=0, style=wx.RA_SPECIFY_ROWS) + self.hyperlink_about = wx.adv.HyperlinkCtrl(self.panel_settings, wx.ID_ANY, "About this program", "", style=wx.adv.HL_ALIGN_CENTRE) + + self.__set_properties() + self.__do_layout() + + self.Bind(wx.EVT_CHECKBOX, self.on_checkbox_monitorclipboard, self.checkbox_monitorclipboard) + self.Bind(wx.EVT_CHECKBOX, self.on_checkbox_stayontop, self.checkbox_stayontop) + self.Bind(wx.EVT_RADIOBOX, self.on_radiobox_theme, self.radio_box_theme) + self.Bind(wx.adv.EVT_HYPERLINK, self.on_about, self.hyperlink_about) + # end wxGlade + + def __set_properties(self): + # begin wxGlade: BaseSettingsFrame.__set_properties + self.SetTitle("Settings") + self.checkbox_monitorclipboard.SetToolTip("monitor clipboard every second for hex or dotted-quad values") + self.checkbox_monitorclipboard.SetFocus() + self.checkbox_stayontop.SetToolTip("always stay on top of other windows") + self.checkbox_stayontop.SetValue(1) + self.radio_box_theme.SetSelection(1) + # end wxGlade + + def __do_layout(self): + # begin wxGlade: BaseSettingsFrame.__do_layout + sizer_main = wx.BoxSizer(wx.VERTICAL) + sizer_panel = wx.BoxSizer(wx.VERTICAL) + sizer_panel.Add(self.checkbox_monitorclipboard, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2) + sizer_panel.Add(self.checkbox_stayontop, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2) + sizer_panel.Add(self.radio_box_theme, 0, wx.ALIGN_CENTER | wx.ALL | wx.EXPAND, 2) + sizer_panel.Add(self.hyperlink_about, 0, wx.ALIGN_CENTER | wx.ALL, 2) + self.panel_settings.SetSizer(sizer_panel) + sizer_main.Add(self.panel_settings, 1, wx.EXPAND, 0) + self.SetSizer(sizer_main) + sizer_main.Fit(self) + self.Layout() + # end wxGlade + + def on_checkbox_monitorclipboard(self, event): # wxGlade: BaseSettingsFrame. + print("Event handler 'on_checkbox_monitorclipboard' not implemented!") + event.Skip() + + def on_checkbox_stayontop(self, event): # wxGlade: BaseSettingsFrame. + print("Event handler 'on_checkbox_stayontop' not implemented!") + event.Skip() + + def on_radiobox_theme(self, event): # wxGlade: BaseSettingsFrame. + print("Event handler 'on_radiobox_theme' not implemented!") + event.Skip() + + def on_about(self, event): # wxGlade: BaseSettingsFrame. + print("Event handler 'on_about' not implemented!") + event.Skip() + +# end of class BaseSettingsFrame + +class IPConverterApp(wx.App): + def OnInit(self): + self.frame_main = BaseConverterFrame(None, wx.ID_ANY, "") + self.SetTopWindow(self.frame_main) + self.frame_main.Show() + return True + +# end of class IPConverterApp + +if __name__ == "__main__": + app = IPConverterApp(0) + app.MainLoop() diff --git a/GUI/IPconvGUIbase.wxg b/GUI/IPconvGUIbase.wxg new file mode 100644 index 0000000..3520d5b --- /dev/null +++ b/GUI/IPconvGUIbase.wxg @@ -0,0 +1,227 @@ + + + + + + Quick IP Converter + + code:IPconvIcon.GetBitmap() + + wxVERTICAL + + + 0 + wxEXPAND + + from .resources import * + #eeeeee + #000000 + Click and drag to move the window + + 4 + 4 + 0 + 0 + 1 + + 1, 2 + 0 + wxALIGN_CENTER_VERTICAL + + + on_checkbox_reverse + + #eeeeee + #000000 + Reverse the byte order during conversion + + + + + + + + 0 + wxEXPAND|wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL + + wxHORIZONTAL + + + 0 + wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL + + + on_button_settings + + #eeeeee + Settings + + code:SettingsIcon.GetBitmap() + + + + + 2 + wxLEFT|wxEXPAND|wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL + + + on_button_exit + + #eeeeee + Exit + + code:ExitIcon.GetBitmap() + + + + + + 0 + wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL + + #eeeeee + #000000 + + + 1 + + + + 1, 3 + 0 + wxEXPAND + + #ffffff + #000000 + Dotted-quad IPv4 (e.g. 192.168.1.1) + + + + + + 0 + wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL + + #eeeeee + #000000 + + + 1 + + + + 1, 3 + 0 + wxEXPAND + + #ffffff + #000000 + Hexadecimal IP (e.g. c0a80101) or hexadecimal value + 1 + + + + + + 0 + wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL + + #eeeeee + #000000 + + 1 + + + + 1, 3 + 0 + wxEXPAND + + #ffffff + #000000 + Decimal IP (e.g. 3232235777) or decimal value + + + + + + + + + + + Settings + + + wxVERTICAL + + + 0 + wxEXPAND + + + wxVERTICAL + + + 2 + wxALL|wxALIGN_CENTER_VERTICAL + + + on_checkbox_monitorclipboard + + monitor clipboard every second for hex or dotted-quad values + 1 + + + + + + 2 + wxALL|wxALIGN_CENTER_VERTICAL + + + on_checkbox_stayontop + + always stay on top of other windows + + 1 + + + + + 2 + wxALL|wxEXPAND|wxALIGN_CENTER + + + on_radiobox_theme + + + 0 + 1 + + dark + light + + + + + + + 2 + wxALL|wxALIGN_CENTER + + \n# Copyright (C) 2014, 2018 Brandon M. Pace\n#\n# This file is part of Quick IP Converter\n#\n# Quick IP Converter is free software: you can redistribute it and/or\n# modify it under the terms of the GNU Lesser General Public License as\n# published by the Free Software Foundation, either version 3 of the\n# License, or (at your option) any later version.\n#\n# Quick IP Converter is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n# GNU Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public\n# License along with Quick IP Converter.\n# If not, see <https://www.gnu.org/licenses/>.\n + + on_about + + + + 1 + + + + + + + + diff --git a/GUI/__init__.py b/GUI/__init__.py new file mode 100644 index 0000000..07b437d --- /dev/null +++ b/GUI/__init__.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +# Copyright (C) 2014, 2018 Brandon M. Pace +# +# This file is part of Quick IP Converter +# +# Quick IP Converter is free software: you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Quick IP Converter 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with Quick IP Converter. +# If not, see . + + +from .IPconvGUI import IPConverterFrame, SettingsFrame diff --git a/GUI/raw_resources/ExitIcon.png b/GUI/raw_resources/ExitIcon.png new file mode 100644 index 0000000..10d117c Binary files /dev/null and b/GUI/raw_resources/ExitIcon.png differ diff --git a/GUI/raw_resources/IPconv.ico b/GUI/raw_resources/IPconv.ico new file mode 100644 index 0000000..7eecadc Binary files /dev/null and b/GUI/raw_resources/IPconv.ico differ diff --git a/GUI/raw_resources/IPconv.png b/GUI/raw_resources/IPconv.png new file mode 100644 index 0000000..6c804ff Binary files /dev/null and b/GUI/raw_resources/IPconv.png differ diff --git a/GUI/raw_resources/IPconv_attribution.txt b/GUI/raw_resources/IPconv_attribution.txt new file mode 100644 index 0000000..1f5a425 --- /dev/null +++ b/GUI/raw_resources/IPconv_attribution.txt @@ -0,0 +1,2 @@ +Main icon author: Arnaud Nelissen +Main icon license: Creative Commons Attribution-NonCommercial 3.0 Unported (CC BY-NC 3.0 - https://creativecommons.org/licenses/by-nc/3.0/) diff --git a/GUI/raw_resources/SettingsIcon.png b/GUI/raw_resources/SettingsIcon.png new file mode 100644 index 0000000..335f373 Binary files /dev/null and b/GUI/raw_resources/SettingsIcon.png differ diff --git a/GUI/resources.py b/GUI/resources.py new file mode 100644 index 0000000..d6d5ce2 --- /dev/null +++ b/GUI/resources.py @@ -0,0 +1,631 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +# Copyright (C) 2014, 2018 Brandon M. Pace +# +# This file is part of Quick IP Converter +# +# Quick IP Converter is free software: you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Quick IP Converter 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with Quick IP Converter. +# If not, see . + +# Main IPconv icon stored below as IPconvIcon and IPconvPNG +# Main IPconv icon author: Arnaud Nelissen +# Main IPconv icon license: Creative Commons Attribution-NonCommercial 3.0 Unported +# (CC BY-NC 3.0 - https://creativecommons.org/licenses/by-nc/3.0/) + + +# Data converted with img2py +from wx.lib.embeddedimage import PyEmbeddedImage + + +ExitIcon = PyEmbeddedImage( + b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACv0lEQVR4Xl1TbUjTaxQ//zmp' + b'NivJyPeM2v4r1CxFifZBg8JiKCK9YCBkl/qgdEGNIZVBXDJ8wVX0QZdLSytT1kBEMD+ocLlg' + b'1+VVMcNcvm3zPZWcMtZ2Os+D27oednbO+b1tYxt4q0d5SD2ijMb5hw+eutd/hMC2QrdbuqSr' + b'fMQ0epVK7cV95mEibBWPcFh5ECf+yP2ALpfsd81saUkt01hJM0QaFuIzDxBg11XhmsnIJ7vH' + b'rl7u9oZMa4vq/9um6feGmMUY/sqrb9/gajPr1/xmuKXoz+aZsr90ZhJbK8o4t7Klo3fCQwRr' + b'dWW5teaZNir3GsgiowBAoEZw2Kxgb2xgK0QQFxQdTTs/iZsB26sG2HdB0ya4NzflltI7zxfa' + b'TDkxFy/BrvAIHsJqY9bOw+ThkWREQgWOTbe2gDwxqS+2pi6LK9GD0tHCW00LHe1XDmk0IDsQ' + b'ykMEbvPXxsI8THZ0QNCJxH8SDPVZgbv3LPKArRDJ6N2SOpuxJU+Rmgo7Q/ZTgL82l5dhvLcH' + b'glNO9Z6srcuWyuXfGS71CgSJ4Bl78njuJ9k8DgdgYCB4/H7C1sFFnDQsfClAJlvx+bzLSEV5' + b'9bhBX3j42FEIDpIDIGOJRvSFzC8ugX1yEsI0GabkKl2OECBxAno8khFddblRdQQHz6XhVEY6' + b'TmWe53PwbBrH+M07nd9GUYF9t4uNbqdTBl/0tfebVQocUKfgxJnT1Gr8RpPd70QFtRI/0U4Y' + b'7wkvp1Lix9J7TdBEizkxHi0px9GSnMDbnBSHDO+6ntf7d3FRK+0M45pxapp0x3MNsJ/jS1r6' + b'Y5X4NY5PZPf7zIzPzrW1UEQM6Lp5o7NBJfo0Zq4R/f8HthhEEf9VxqCBCFN29hAz+74Bl2tH' + b'V35++wvVlkb0mf3FAD0RnQUFxvW5ucOwrVwOx95urbaRdP8z/wJ18ad1OOLfGAAAAABJRU5E' + b'rkJggg==') + + +IPconvIcon = PyEmbeddedImage( + b'iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABHNCSVQICAgIfAhkiAAAIABJ' + b'REFUeJztvXmUZMdZJ/qLyK326qre1JtaiyVLLVuS3bLUtizJWJbkZXi2OSNsvMm7/RjmGQMG' + b'sxrw4fjMzGPO4R3mYYwNBg8wIAYDjxmwjQ0yGITwbuMNeZEsuSX1WktWrvF974+ILyLyVlZ1' + b'dde9mXmr7k+KzsybWXlv3ohvXwIoUKBAgQIFChQoUKBAgQIFCmwHqGFfQIEC58LdQOngQVTX' + b'er91+ZHS/3vfV5YHeU1bBQUDKDDy+Nm7v/+H3vHyF/8WGNzv/fJlV5dLs/N/1fr0X/6P9/zJ' + b'Rz7yn+79m4VBX2NeoYd9AQUKnBMlVFWpMqnKlal+A2Sq7Ue+81J98Ol/9PM/956HG9/48h+e' + b'+eP//NJ3POvJ08O+9FFHwQAKjD7MBj7DDO60YM6cnGkf/+7LS4du+NNffO9//07ja1/83YU/' + b'+C8v+uHbjkxlfp05RMEACuQCvM6w/yg7FAClwO0WumdOzrcff/Q16tKjf/lffv33Hlz56ufe' + b'f/b33/O8H7/j2snh/IrRQ8EACow8NqYAECwnUNaxpZS8AW410T35xN7OY8ffoC658aO/9Ku/' + b'/bWVL93/G6d/91due/uxY+OZXXgOUB72BQwbd999d+nIkSf6OkOPH3+yOnPmDN17770bWYMF' + b'sgT39f/Zt8BC+14DECbAAEAMKLKfaTdV91TzIIC3lq945lvf/b47v/Nu0/yLT/zph/7HZz75' + b'kc/90n0PNbP/MaODbR0FOHLkSPVLv/cb/4BSub9KWKtV9K59v672HPh/BnxpBSK88+7vv+ed' + b'r/iBD671fungZWSWlrXSCtAaSq1e1gwGyD4TZsLsGAcAPT4BPTn9b7x86s9/5UN/8se1k5/6' + b'wi/d+5V2Bj9npLCtNYD/9h9e/0JVqT0DpVLvG7JAJmcZ9eUbh3BpBRJYW/67N5kAVu6lEkOg' + b'F5qjzwfpx8yglWXQyvIVgPqJn37Vy3+8PPXmr/3Ejxz/8C996C/ufWhh4UtbVQvc1j6AW48+' + b'7S3Q7haI+ihgQJcqAHWfc/fdd5f6/X2BUQGDmcEMMNlHYu4Z/n22DIIBsFKWZ2gFKO1MBgY1' + b'VlT7xPGru139Mz//hld+7oO/8JOfXbz/o+/66be+/inv2mI0s6V+zPngXa973WUoV24PRyIO' + b'wADGJ4BuR4Gw74o9c9cN4xoLRFgvDCCC3VP4uvqChVK9TF/bY0op/xxgUH0J7cePX2vq7V98' + b'52te9cWf/NKn/2X5U3/9zj/5sXuuSvcHDgfblgH8whte9lpVqVV6j8riYfDYBKPTBgD9K+/8' + b'qduGcIkFIsS0vWoAzgRwhE8MEG1gBB+AO4uNJDpGoJxD0X6nAdUXVfuxR57eaXbec8fdr/3y' + b'8hcfuH/p7/7sx/78x9946UBvRorYlgzg6NGjFS5VXtV7NFpRumzDR0RgJlCz8bzhXGmBgHXE' + b'PzOY2M9Zz1yez4hhwwhQ4MAIlFMZjIFZPFtqHX/0pk5X/+pzX/6aXxvgjUgV25IBvPhpR+5C' + b'bexSdsnlDFkD1lakiSlWzYZ4lACiZx89enRimNdc4Bxg9rkAzOeyF85jiJ/AM4HwqJjBnQ66' + b'iwu5jaZtSwbwM/e84i1Ki19PJEBQCXWlomKJoJSaef4znnZsOFdbwGJtImUheo7nMoXBdqge' + b'hgKfcGgvK2lG5AvbjgG8682vvpgrY3dwj8rn1EdmYHwS3GlxSCqxn/jln/yJwgwYJs4prMnO' + b'n2PiF2IBrGsdRPQfLskepA3lKo4mtl0ewC1XX/VaVa3VAHEeuX/EhhwbZ91aUau4eqfzfQO+' + b'1AIejrjXUrQZdv5AYAUoYhvay+xy2DEFpzVSdqfKGttKAzhy5Ej1tmccfZVn5WL8k3vU2kaI' + b'hfh71hDf8NP/8T/uHugFF7AQAltHEpPIY3mfOLvhnUbw580rtpUG8LIbr/s+VMauYEkD86lg' + b'zt6b2sFanH/yng0HQwFl1W48G8CHB3/lBcRkC7Z39EKkMdvPkdKQrMCMriY4HePwYw6xrTSA' + b'n37Ny9/i037FuBNnDzF0pabWnExmvPsn3nbX4K62QA8kPyP6r9fpF4cCM5T+7jzcc5782gDb' + b'hgG8+7UvO8TV8TvDEbdw4Oy48Umg3RD9DkHPjF53u7cVacHDgfX0RzSPhDUgjEAeYbIb8Xk2' + b'mnk4otg2JsCxa65+larWJoPuGNtyDDUxSWg2NEuIp+dj3ilw1eW7d18K4MGh/IjtDG/ch3Rt' + b'hnvpE4A0oBhKA5yxCcAUzqsKDWC0cfTo0cqtTz/6GiCSGs5eZGKwLiHkfEbSRRYZKy9qnnvj' + b'Dc8Z6MUXsOiZlGj4xD+RyGSZAWc4iAA2LlNUEo/yiW3BAN7zgy+8FbWxq2Kpzz6EQ8DUDKvm' + b'ihLnjnj9oBJmADNuv+lpLxjaD9nOEOJblQBg07X9IKeWZ+kDEMHhzkk5jgNsCwZwy7XXvlWV' + b'I2vHJ4zYxaIqFe9F7vlMnAHi1AYmfubb3/72bd1GatAgmEgB4DAgU+YYuWHPBIizGxw5ju15' + b'82sCbHkfwM+88qX7MDb2AgZDsbJSXWiaCGp8Etp0lbcLlHKEH9mQonoCUOB94/X6NQA+PfAf' + b's11hEHIBeno2OI4tWp0iKLJ+AN/8IwsQvPQPQiKf2PIawM/94EtegerYJKKccTuMtd0mpxjt' + b'Jnq9/vIYDRWO3Xrs6bevOlGBTGFTfUTVFw0gUsWj/H1OqxZgjeEJX86bYw1gqzMAxdXx1ztn' + b'vmMAYisyoDSU0sErKNpBkvjjYwzccdMNzx/Oz9m+8M62yNknxMi+dDua39hUSH2ENSTnzSu2' + b'NAN418tefBtq41f3JI0A1pFjCJieZbVSVz2BZWC1n8kfs99BXTr6rre/YX7AP2f7wpjVSTje' + b'+y5mgJXGcTJQVkPCfz4TMMfFAFvaB/COH3zJm3Wl6pR3duE8t5jA0NUa0Kj7rFGv5fsXiJiA' + b'FIAwlMJ0ewVHAXxscL9me8OnAqsQkhUHoJgArocHGNy/KWh6V2Np3jiHoyk0gJHDT9/9/N0Y' + b'n/p+jqV7lDKqapNQ3W7P3zAQmj/Ex+AYSMQUfvltP1KkBQ8Q/VTxEJsPGp6EdzM1AaIIkpUK' + b'RTnwyOGWK578Sj0+MQVJ+SXX4Idt6EZNTbFqNZQXGwJmcLKvfDL5hAHVbhaOwIFCxH5Q0ViF' + b'Y0w2Gq+VBmvKVgNgdrKEfPgxr9iqGoB69tHrXidOu+D9dxOmNJQu9eaSA+hZZPFIfkoBxOrq' + b'n/3h1x8ezM/Z5iBAmnh67z+CNkfMICIwGfeY7ZBzwdjzgfOrAWxJBvCzL3n+zWp8+qk+ScSr' + b'/85WnN5hM//YfwLhSdIDyIEvyEeIoUC1W264sWgTNgAYxB5+DoGAWA2nEJpDhg5AqQD0Dkjp' + b'LpxTbEkT4B0vfdGbVLWqgp2oAacFgAil8QmFlWVf7AMlTqY11EbPGBA0Agaee8P1LwDwR9n/' + b'ogKhH0BUyGXfiGx/2CnUURZ3ViAGGRsJMJzfKMCW0wDedfed82py+qUh5x8+bgtjM//QbffG' + b'/eI4bh8FILzXGydUHXPrbbfdtiWZ6KhBcgAk/BbH+8EENiEZiMlkmgpMZIJJYPKtAWw5BvD0' + b'iw+/Qk1OTbMvE3OLwtiFoadnGK2GElXSE/Va6r83A5y5EHmDiczhY0++9OqB/8jtiL7eePQm' + b'5BCBJMybYSYgXPahzwQsfACjgbuB0rOvfeprAdUTsmE21mmjFHSp3CPVPQ9Y5elf7fm3mp7k' + b'EwBKKf3uH33brUP5sdsNJPMgRTicYAqSAhxCgdlVA0bOQKd95BVbSn29/IXPuUFPzR5lEBRK' + b'doEgqI56flfI/ANC5g8rG/pbT5PzmkKkETCAlaU7Afy3LH/XtoeB9ba7bbs8JB9AJL5yvJnt' + b'Xp+ZgUI4GUSgIhV4NPD2F93+ZlUbc/TpdoUV9d8QyhOTChSpa97GP5f07+MrcF/AxDcfO3as' + b'KA/OEAYGMkcifeNmHJw4jvgzWQwfUTLueWECDB3vfNGz5/TU7EuZXaPmKOzHZKDHJ6C6nUiX' + b'd8MTOW1gcO9zMBTzztuuvuLokH/+locvuomz/iQEFzkBAyPIMhQofQHYqv+FBjB83HDgwN2l' + b'6R1z3i4k9skabAxKs/OkWo2g/jO8Lb/hAfc38gVuIf7y//XDxa5BGSP4cxKOWCan6Yln3vho' + b'QGblwD3P7frKK7aKD0Dd8tRr3gilbCEIE9g5Am2CiEKpUlHcbto94IWalXPenC8D9w4o2MfW' + b'yvMA/GJ6P6dAD4zt3aDAYFesYd03wf4XR5xS2vkCMk4FtlEgsCFQjp2AW0ID+PHvu+np2LHr' + b'aDDnnXpoCGwYpfndjJUlxU7t98Lf5wmcz5BUVHbfRWCioz/25jfvGupN2OrwcX6n8osK7rMD' + b'k76BZL5AmkMyAqNchJxiS2gAb3vesTeXxic0M9udXF1ihthrlakphZW69xKvvf3XBiAhQcCb' + b'BorV2O3XP/XYfwX+Mr1fVSAGwWpymsjv+xfMAQIZY6eSbBZgxgqA0wDIqv85dgLmngEcvWxu' + b'tjy75weYCErBhmScnc6GoMcnoQy5WB888doiQOn/dx6Q8J93CtrvvP3oU56PggFkAxd2UwAI' + b'ym7+iUjTkxwBMta/69KBswJLGNDIJiEFAxga3nHTsR8o7di5y8b8pauXFIQQKvO7GI1lxUol' + b'mknw+RN/H/gU1XbruZv+sgJ9QSBw10p+pYIfliGOQWuPK2hoUpb4TXYqALsiIOkQrIpy4OHh' + b'mUeufhO0tru0spRpukdWKFerCDFka7cH6X2Bw1WehZZUDDCufOfrXnfZcO/GVoYjdknx9sQf' + b'PPGS7s3GgCi7wVIGbJwJUEQBhoMfe+bVT9O7L3qG9fy7pB8oH6qp7tkD1ZDMP5sellazKOsD' + b'IEgusWKUbn76kVvwO/hWCl9fIIFQfqtgoz3wWh7EKQey2rgegA/A1x+IIMgncs0A3vqsm95Q' + b'mpgssyNERdqp/wQiRmV6ltGoW4pXXm9MTf23XsXgC3jedU95AYDfTeGLC8QwABmC0qL/27kj' + b'n/NBYOoCKDlGcP6R3fNFyDmwWkFekVsT4Mju3VPlXRf9YG91mIvLdgl6bAIlUdN9Eo/L5Duv' + b'7J81hs8GjLzRhm5517velWumOpowiTCfs8K8BLaxeHIdgchE5bpZDPf9hoztCZBjBpDbxfqT' + b'NzzlByo79+yO00Ktc96ujtquPYz6koKyveMUO9ewDwFsEl4lFYbAAGP/yoNfvRbAZ1M4QwEH' + b'A1h1X7GdO6m/d6E48pWACuw98hk6Ad2aI+OcgVQkAg0cN1195RtZq17JQAQ2XTAUKrUxRITp' + b'6DXqEbDZIY5FICgGYNzytOufM6x7smVhpNdf5PiLavFta24nmbvuMevRtfF/irIQ84hcagBv' + b'veqqp1T2XvQsmxGmvHfequEGtd17WTVt049ki+90jUPJAwgRgtuvv+ZOAP81zbMUAJgNWJWg' + b'fZ5HqPnw6cAAmJyPIFMnYCgKcrZIdifLGLnUAN56w3VvKM9OlWLbn8iAul2QIdTm5sCdVvAc' + b'J4g0tRGr/z4t2DzrHa9//fRw79DWgoHc4t5UXKv1wRYEGQOmLoi6VgukDIcJj2QMTBEGHBze' + b'fvDgeHXfnh+yHX5KnvMDbPP+JyZR9o0+gND5Ez0e5HQg55DCIACMabQWbgTw8RRPtL1BNgqg' + b'tY4KuMT56wjf2PWg2W4jqjMUysw2ykTEIGOgc8wAcqcBXH7o0Esq+3bvDdKfnA1oQMagtmcf' + b'o76sZLs2iROHllLpj5721Mz4xTe9odg0JFWQI7rg3ZdmL2RCGzDyHvpsE4G8P8L5IijHJkDu' + b'NIBnXnX5m1RJw5f6uhJgGAJBoTY+Aa4v2f3hWUPBOgoBlXqraBbvn+SGM9nn9ZWCAaQMNgYo' + b'6WDjg0NPPnEIwuYJ2XTglCc7vhZyWoALBxbbgw8Ibz58+Krawb23iifeSwUn/au79rBqNxTH' + b'qnkIAcDvMZ/SCJEA7nlOiq7/0Ve+ct9Qb9ZWgokcbxyVAMvOQK4m3zrljDMLMswD4C5CzokB' + b'57gWIFcawGuuu+aNldmpEjFBs4KisBCIGJO79ig0V2zWn4IjfO2LhLKAZzYuQQVMUIxqudN4' + b'FoD/mc1Ztx+4x9zS/lhsBrICiBQUKPNUYElDZmaYHFcD5kYDOAyM1Q5e9HJm2x3Gcn3jH0tj' + b'4yhrxb5fu2QAMvmFk8VAnF0YNaf4ude/8gXDvmdbBYReiQ7Xiy+W9OSHiZ5nPMTvkOONQXKj' + b'AfzI9df/u/G9Ow8wsRXn2ub/W8cPY3r/QcbyovJtwRAl6WQJOQmLNuBGq3nLbbfdVr7vvvu6' + b'5/qKAueAgbfxWSvrdGOr9fXk5IOhRNXLUAMAw3WaNj4ZKa/IDQO48YpL3qIqJfief2RJnI1V' + b'A8empsHLi445aIDcYriQph/nC58R6FRU6xS84sY9s5ffB3w925NvD0j/fyJGKfLtSE2+5ONb' + b'h7zKnAGIFsBk/U95RS5MgFfv33/l2IG9t5JT/6Tgg4yB6XZR27sfulFXiMpzV20jlfGAZKW5' + b'8ysmdfP119867Hu3FWDgTD12889sAy4+PViKgSQ+H8XpMxjGfX9IQc5vFCAXGsAPXXXla8d2' + b'TFYBy/EVuZZfbgImd+9hajWVUgpQBOWKfrJ0/sVg/684qazSces1V74QwG9lfwVbH8QMTQzW' + b'ZE0CV4gl0p/IQKtIG8/QC2hTzm0SEJHJ9e7AI88AjgHjkwf3vEKaMGoF2PZ+1hGkxyZRK5fB' + b'LUnFszkCSnaRHgQHgDsXCytwC6LTuenYsYPj99//SGMwF7FFQUJ0BNau5ZdyGoDLByFiKEUg' + b'DShSmUYBLO+RMDTnOg9g5BnAXVdffdfE3vnDzGSTeqTogxnUJcwcOsy8vOiSf22DDuVCf7AH' + b'B4TYDJDcA+x75qHrnnr//Y88MKir2JqwTj7WGswaAEXJOKKOM0hZrUDD9O4hmDLiHAQmhslx' + b'FGDkfQBHLzn4Fl0pewngw3+dLpgZk7Oz8FtCwTrgIFmCAx3szx0nrfz8PffcMex7uBXgbX0J' + b'u0lSEIIJIKW53ieQ0ZDvN64JSREFyAgv27378pmDu59DRNBaEvqUXwgT+w9BN1aUtAQH2xRQ' + b'dn3jBgpR/6lXE+ClhTsB/MpgL2ZrwbgwIEO7tF9nAoozTpKBYHOEiJCpBiBMHq42Ic8+gJHW' + b'AF58+aX3jM1MjvmiCzIg6sIYA+oazFy0n7jVDIk/rirP5+UPcrj6dO8B9AlIfP07X/2SnUO+' + b'lTlHKADy+f/xo7QA82skwzRgr4WEtuCFDyADHAUq0/t3vVrcaraeR9J+CaXxSdQqZUUt6xsI' + b'4X7nKxiGWSb5ANKswjKEmRuedOQG4M8+MoQr2jIgZigiu+8DW0dvHIozZFOBrQOQs3X9sISi' + b'7Xl1wQDSx3MvvfSu6T3zl1guD8Dl90kN9tyhS5gXz1g9z7X7htv8I9ONIdeDTwiSEmQbErzt' + b'yZfdBaBgABcIA5cJqAC4zs9iAoi2x2RsLQAroAu/fVgmIFf05c5bbA2WAa49tO8t5WrZ2lla' + b'uVCQq75SGlM7dtjMP60AafqpXGOOwbn+V4NDeTBLkVCzUTgCNwMDEDG0jqoBgfCcpBDLZYZq' + b'9n1DskDcisybnDnFSPoA/t3c3MVzB3bdwWRcPQ/5kl9DhIm9+1DutOw+X1GxT9gjbrijt0gI' + b'YOIr3/biF18y1Juaaxjn7XcFOCxEGDL/jBu+SCfDwRzOZYpEoPTxwoMXvWZydrLmJ9kdlxDg' + b'7P6DTI0Va/i7jB9f/TsK3NinCEOYUvXmI1c869f+HN8Z9qXlEgYgY2s7bNmv8j4W6Qxk14mC' + b'1gZkFPQAGoIYCppAXjGKDKA0e9Gue3py7Y31tBPZzL/xWhXUaUBD2biPUVbtA7ItAtkInB/A' + b'hgGdrQrGLVccugvAHwz56nIJm17hCN1osKLota0GZWMZA5G2TICyTQQimV9D4G7BAFLDnXNz' + b'd8xdtPNJcbMHKIYhgLsG84cvZTp7WoEBKmm7J6Bi6x0elvMvAalck3wAZ6s+F9bkyq++ODS4' + b'3v+q5P0AADvBy/4/aRSTuU3OUSUiGJTjKR05H8D/cdmhN1cqJe/ZJWNTfqnTBRFhZm7exmFt' + b'TSZCc0Z5HP4QL3F8TcS0/20vvOPaYd/fvMI7+yDxeO6J+4d9Ahnk+rNkNUICqDNBhul03iRG' + b'SgN43vj4/l37d72AyXpyxZlGzDCGML3/YuhWw5oEWoG0hmKCIpH+GdeBbxQ+GSlOTGJ97MmX' + b'PfvX/jc+P+zLyyPIOVWJtGv6IdqVRANsrghre9+zzM+35yQ/z3neGmykGMDFu3a8dnpmakzS' + b'PW34X+q7CTsOHGBu1BWUBrOyXX9Ytvx2E84YfBpwEhKKlKIgZw7c/KRL7gLw68O9uPxBPO5K' + b'wTsBCWFdSDRAKSsstEsGyw7igAypyHnFyJgAR4HK0Yv3v9r2gA8OFiIDMl3o2himajWf8gnh' + b'+E4f4zgUZ1x65rBCgZyQTtInoNu55e133z0+7HudS8T+FPJp1pDNYH1EQMyATIuBJPogjGDY' + b'N+fCMTIawPjk5K27d++4yqvOhlyXV6v+X3TpFWwWTikwQ9n4mpP82jv/WCX4/jDDAmIb+gVK' + b'gMFs98yJGwHcN5yLyi98+I8IpBVgYp9PiBiR07h01uXAYt65dOS8YmQ0gDsO7X3zWLViVX7Y' + b'Cisiu++aIYMdO+cUG2NvOuzWzGyCGkiO6xsjSSH2M72togY5yF8fu1oGBuMn7n5RsWnIecKm' + b'ArN39skakWOibfmcAMSxgSz+c9eTa/efxUhoAEcnJvbt27fzhcTGhvOMTe1hInTJYPrAYeiV' + b'OrMhpTSDUXL2IGzPr6Skd68Z8VtD0ARcYpKtGLM2IxYXngvgFwZ/MTmGMTBglETtJuNj8YEx' + b'kG0UrUtgIpiMy4HJaanEBFP4ADaHyybHXzk3Oz3l1TgKtj91CbsOXUxmZUWRu/Hk9gbo2SEm' + b'VgX7vabV6mL2w7jUUSex7G97xn94ye1FefB5wu8JIBmW9qB7LeXA1jCUiEFWI/h1hLkXxUCb' + b'Qfm6A7vuUS6sAxiX6gl0yUBVa5iq1ZRZWbaef9a2KEyRbfoQ9/zrowlw0h88yAiBqKNG1FQD' + b'EKp0ZuUWAH82uAvZAohCqt4JxwzDov4DAKHk9gXMUiaL7U8uGSm/8n8EGMD1Y2M379s19xSb' + b'8adgWLkkIEbXGOw/8mTqnD6pASFtgt3mG713Xq16MnRTQJyA3jnlHFc/9sLb7/yN+/6pYAAb' + b'hO2+a8OAhrQ1/yD2f5D+TLB9AcFQWZcD+yiPTQrKK4bOAG7Zs/OtE9WSy5YDlLF3teuce3M7' + b'dylePGvJViuwa/kNUK8091SOxDH7RngqXCNrRsAhUcQnBTnG1mzeejdQutf6twpsAHIfS7H6' + b'zeEesyQGlVxRUIZimRGckHl3BQ6VAVw7Obnn4r1zL5LJVcRgGBhiGGMwvf8Qyo0miI0jduVC' + b'gLI5ZPRlsmt0krCd1PdvR8eyBUfpo+KDEB8HH5m945aL8bG//3bGF7F14NRu0feT/hZDBJQA' + b'ohI02CuJmVyKy0EhVxFIRVPQC8Ouiv6hXTPj0+TSeY0jli4ZmC5hz+FL2TSW7IYfrimIp3Sd' + b'VPXXgeI+voB+KkNaCFmJ8aKFqyBTxOqZFx9+zvtRMICNgphQYm1NAS12OPnQ3yofQcYMwNcA' + b'cL6dAENlANfvmn8D2Nb5awZY2dBOlwiqWsXM5ATozAoYQIkJrDSUSwCyXp9+0r7fbFimwT2l' + b'AgkHYprgcB1xVJpcbgOIceO+nS8A8DvZXMDWgiG7RhTsxqCKlI/o2BCgsRqkaASKM9bvRJOz' + b'uQeU41TAoTGAy8rlZx/cNf0UhgFzyeZ7Ozu5awgHrrmOu6dOKiiGBsBsu36SgZUAwGoC7jsP' + b'atUbSv7NQgng8MQmjMCFjgDpb2hj151j99x229jv3ndfM+Ur2JLwSTjO22dDqwiSH44YhRFk' + b'bQLIQL7ru4fGAI7unH3TZKWkLGe34Tqp+usaxu7de8DLZ6GVDgTvnHi++GLDjFf18AGVPJ4m' + b'Egwg+AA45K1blfVQqV2/GsDnUr6CLQhb10GKAC75+9rTops4dI1Gxvn5TuqL9OeiJdj54SAw' + b'f+nO6ZfY0I6GcuExYkbHEGb2H0St27FJNAq2tstPqD4Pwhf0Un4Q/Krn+KbB8dNgG0qDEJ+7' + b'7jrZ3HTpgdt/+58+XTCADcBHAIgAUr0SmBjGOpBgFAGEzGsBhPiNkXPnE0NhADvGa6/YNzU+' + b'Y4yr91dWRTZE6BiD/Zc/ibv1Bev8s+ldAMi1/F7H47rePPRdD1lpAFFgyOmn5NV/29eQmXHD' + b'7p0vAPB/p3wFWw4G8M4/63mXdUF+oxAxEsAKrHpmIHXYFmWOsefcCBgKA7h6bup1TAQDBtiG' + b'9IgJHUNQ1Rrmp6fAZ0+BoaCZe83485nXOOTf9++cLpCqBsDRU68COC2AQhcZW0v+9DfceWz+' + b'Ax+9/3RKV7Bl4eP8kk8R5VfAaY/K+1qs1pjZtXC4DikLzisGzgDmgWOXz05dT90ulHYNHmGl' + b'f7trcOlTrmc6c1oxM7RyHn8Z66h15zsFmfgB+kl/wDuupICEjdiuvOPonn3XfwD4REpXsDVh' + b'jCV6KEtsWrIrpWpUmIE1FUuU7b4gLqkThsXxmN25ssbAGcAlM5NvnClrbVxMHC600zGELjEO' + b'HjygaHnB0nrJbQTPbrefNV273OfZRpDiKvF+ydhBKYUrySIlSWFl3LB3910oGMA54dt9uXvK' + b'Ud8/Nq4TEMv7WdcCkM8BIOZiX4DzwI4nT0/8+27X2LJN59kzxGgZg7kDhzDBhg2RUm47YLsf' + b'vGTynWNauff9cy8ChkssTwe8hvRHSFDxlYlktYHO4sKdAH4qpSvYknDtr1GzAAAgAElEQVSp' + b'E1Au+06zWw0cpeO6PCsQ236SGV4PAxKY8FmBecVAGcBUWb/s8ERltktWpTOwdNJ1GsAlV13J' + b'nYWzSsJ+TBrWBeRy+Tfi2eUL0AbS8BivQ/ziA7CbV7hHMi6fnK569dGjF3/oM595ePMXsXXB' + b'rgiMndrtm4BKbgAkJVjZRPGMNwYhigVSfm2AQTIAdWRq/PUgshV/juiIGW1DUJUq9s3NgU+f' + b'AKB8VRfIkb9CwgRYj2h51Zwkp6ineDCN+VuTATgzgJNmgItZGx47evHumz70GRQMYD0QwCrO' + b'/w9ZeESuC7AisNsXIOv9OiUL0RDnuiHIIBnA068er97Y7drCHnaatyHLAK582lGms6cUkYGG' + b'bPghNf/BUbcm2feUBjiq3ki+UCrxYo4UD3b/u8fYBGBpY8Wh4SkRrpufej6Ae1O4kC2LuLkL' + b'PCOI8iqcX4C06xmZdR6ASzayLSzyawMMjAHsr5beNKMVOoZASlp6W/W/S4zLLj6kOstnoZWC' + b'YYLSClopVwQY+ez7tv8CRElwT91n17bvU+XZPU6n1YTvpX/MBDjsLccrze9DsWvQOnBRAFYh' + b'6i4OQIR7S2y3j1eaQ5v4DGAjuqH4KL+1gINjADNHxqv/vuPSfqW7BxOjy4ydBw5iCoYNsdvV' + b'2TZ0YCnljaV/rLuvsgjsAd3zufWxIb/COljVE16YQY/kFyYAMNnCFdvU1O58xEwXv+raq478' + b'9y9+7cubupgtDGGeEuiX1B9JtTbxPXbRpQwvxoUCremR42rgwTCAGeDYpVMTzVLUnAcAoAEN' + b'xqVXXtHV1epZvWPHLEy3Qp0uqNsNGV4uzbMHPSZBLzMgaQ/uNAgx8/vTeooMoA/xh0UZ+QCI' + b'vfrv+gOU3nTzM55dMID+MLBSXwNhgxAv9YUZQHIBQZzttiDWNRXCjaZwAq6PReCjX1LjV8xQ' + b'fddi0+zbXdI37Ruv7D3VaD2rYXjPV//XRw8prXbumByv75ubP7tn9xx2TExOHdg919gzt6NT' + b'0XoMTNMg0qbTAXe7Xq1GgtuLIqBcAxGtEPYN6JNMtFlTsUcBELcDh+M9DkCxYdn4vQJkb/vl' + b'k4/fCeC9m7uaLQqpoFQM0jbeT8x+xyCSRCG3SxQDrnFMNhBxJMlIebbcBuYDuP+RRxoAvuvG' + b'A/F7t+2b2kUt7K4pdXl36ewN3zx9Yv/Jlc6NTeruaHRof61aVXt3TJ/Zv2tXZ+eOqbH5menq' + b'4T2767OT40qDJxXTOHUNqNMBma6LHFi+TEp8CTaEFKeI+WMXimTCSaQBxO2qfHIKW21GCF9q' + b'2ZkY1KFbDx8+PPbQQw8V5cH94JXAuCwYljnYYBEUbI2+0pzpTtEs0QfnDCyiAJvEfceXTwI4' + b'CSx/FcBfRm/p23bPXLbc7OzcU6anPvG9R6/59ne6l5xuta7vGMx0mOfmpicbe3fOP37xnnnM' + b'T09M7pyZ7R7YOd+ZrNXGqNmY7taXQVpDSwN0m1awOqp4gVjLBFjt/HOvoyahRAzT9TnlO2+q' + b'8dGHgE9t/qq2FgxgHcMMECm/RTjZXZcjM0ukclqx3f4gfy5sToCMAEaCAawDuu/E4oMAHvzM' + b'UuOf4zee/yTUnqr2f2P/1NjFJaUn0FgAGguoP3EcX/83xsTMLK697ft48cFFpcFgKBdShE0o' + b'AVJZI6sYgEsTW9W0MmEGsC8Mkh2EGK9+2lOf+8ffeLhgAKsQVftBzCr4KIC3x2VeYZ9ngfD9' + b'URQgvxbAyDOANfHXD6K1/0Cre3B6HEorlJQKdhmk/NbZaAZgtgxAKZVqiDhmAKIaAi5LLbER' + b'ScwMZEureGuz+pmztwN4d3pXt3UQ1P7gaPF8QEJycDtGsoLOqN2bc+nYNeaTvPLLAXLLAACg' + b'y7bVZ0kpaG2lO5GyeZpKeUKzKr92xM+pMYAejz/g1ftEB2B4iRE9J6cFmEgbANNNL77uuh1/' + b'/oUvnE3nCrcOyFX4+bJf0Z7kPsN2iGOGTzJLGxw9sksCMsh3b/dcM4A4L0ACP0pxpAm4xBto' + b'O1vabSqSYq1o8Pazl+jsNgQln5ZqVvsEXGGLtJSyRS0Y4zMnbwbwv1K7wK2ChN0tRGgDQew/' + b'4FOBMwgEegZAUgLszpvjeuBcM4Bgx0s2gK0HlzIC233HEr7SCppcGDDFntFetUe8A7HtYFyd' + b'nmkppWrdto1OdLsdULttd7ph6QsQGoQwAy+76pI7/+LhRwsGEEGqATWHR4kAk6jjLMzfquVp' + b'WwCroz0hHbjQAIYEsfmcjHeSQfZqiT3uYUHYTSNSZABe8sATP7k8/0/8y+d/9dMnTv+fl8zN' + b'dnZOT03umZvp7J2Zbs1Oj1UUeLbbapeMMei02+i2WgARuu32c5BeidIWgjgBHfF51UveC7F5' + b'RjoRnt6zR89ZIjfhnHlFrhmAh1P9VNQx2Nrk1sGmWUHBNhUhqFTzNiwDIKdxSIWYVemfaDbu' + b'f/jE6cXvPnFqL7R6Jis13zLm0A6tazMT40t7Z2daB+ZmSjMT49N7ZqdXds/PmrFy7Ul3X9u6' + b'5N4vfvHb6V1l/uH6AXmG63Qu74gTyc9scz8AuwZS7/cqzkZng3DO+XTuGUDgvtF0u5UgdeOk' + b'4DrFIv0dgTgyA6Itq4gZS4TjDxL9fwDctkfAPHCAdWn+TLONh5ongcdPwikoJV3m8qGdu3Bi' + b'bOpUehe4teCDAOEImI1LAIp5e3qEmfwmYTquyzty3BQ43wxA1D6Cswuj194RyAxNDNK+DCld' + b'95B37IkfgLxE6ofTwKOnO51H+77ZAR555LE0r27LgOD2jtS9TFZyAMQbS64voPjlUtcAEDkh' + b'RRvpsz3wYWBsbP/+w6UylezfVFQVADHr2thYuWJMeaJWW/j41772jZQu8YKQewYQP1eIvfLw' + b'MXYo2yXG1oojdQ8RRUkhNkddWFGBNGAAm0JdYr/7D/n2aqEgSDFQgp0HFZkBwOYYASdehJwD' + b'K/27AJ5/87Hvn5udv+aSyy+9dnxy6pra1PR8qVw+oJqNdqlRr1e6TaoyTTZPnkD7zJmVbqtR' + b'qir+8Me/htdv4tI2jXwzgMgbjGj3oMAEyPWT1zBOE9BKIe2d4/wmlS4UqECOCRRICy6DOzj7' + b'ELz/QSKnL/mB1QxANA+bUcqA0uXnPfvZP1NuN59UUVzRZ75XaT94cpmbjRPtVndqaWVlvr7S' + b'QL3RRGNlBe1OexzEaJQrQ4/25JoB+A0awI6oQxiI2UkNIlcoYvsL2EhgusTJbuWJd1q61BZI' + b'CcbY6r44ldp54uFyKrzOxQwoter+p6kB+EcFKK1QHhu/XX32H0uLK01dX2mg1Wqj1WpOdNtt' + b'3zaMydg24qGFWPORTuufk+caNHLNAEJQyAb/FJQLAZJPApLNIxQxoBU0q0w2jrSqKflOsQCj' + b'k/5ptiV8PwCn+pPbc1lyACRNmBgwygqCflN8IdPej40zGFJvpKBQX1qsfHNpwYV/ZT2GLyjB' + b'Ribk75xx8q8PnGo8cgGXlCpyzQAivm9fJVi1LxIhhiQDZnUZYgLAaSSFBpAuiBldBjTZgKDd' + b'Rt52lLJl1ZIKbDWCfqkem5mRZBgQbMOSJQDGMDRstaKWmhT3GUlLVu7vDCx7mh6rfQxY3sQV' + b'pYNcM4A4FENwLf4lOyxStxgGJbYxIvlcFhcj1X2mp2V0gc2CAHSJoEiBNbs+DpYBmCgaoN0W' + b'8lBr6QAXjmToEQo9RUfSiMQPBthdJwOWERCc7gL805mlj6Z6gReIXDMAAH5mYtNMZoGMy7FX' + b'gHGbi9p+gWmnidlJlgQgiQgUSAe7qqVJ40JtxNqVciiwVigrbbdbg9O8oG06OHoyQzaNVRoA' + b'ZJkpx3CSxO+0EaWcIHIOQ9u25OxceezzQCulq7tw5JoBiCc43FgfnXVE6OLySqGk0l8U/joc' + b'AxAtQMKBBTaO5z/pSTNtbS550sFDOy/fv+dmjE3tVrM7jlGlekBXazvRaZFu1hdrptspd7oV' + b'NOtVU19aocZK6cS3vjMFhlbabyETIkFIO++j/zEVv+eZhPJ2qTULbdhQQ336HxYWzqR5WReK' + b'XDMA6SBsGYGtAw+tI6TAhl2ZMEPZFZI6GOy2ipLzMgr6X42jQGX+4osPYmpq9o4rDz+DJiYO' + b'6p17nq4qtSvU5OQupdRO3Wqs1IgaZe4o1Vqe4MfPNjsri8vN5Ua1UV+eXmx1SvX6ChorK2BD' + b'Ey7cYxm823CGEl7eNKacE096tIAoDCnEH1csimOYmKEIOM346xQuKRXkmwE4z6/fn813i3Uc' + b'lwiGDEoogbVrKwWkv3VsfE62HX8ZQKezPeMAt+3bt6s6XZmmiflr77x492V6fs/VVK1dr8Yn' + b'dqJWO1zqdqlGtFLldleb7hhOn2Hz6HKjU6+fqi+tTC+2mjvr9Qbqy8sw3c6Y9FiwjjcL29zJ' + b'FoErBdf30THeRJg3DS0gyc/Fx9Oj+nsdNLyWxiE+a1EB3202R2Yz2FwzgKTNBSQ4ryvPtY3b' + b'lcsIVOmHAzgKRbn2XttVAfj5G458eLxavouhxsGMzoknwCeeADOjNjWFi/btXTxx/Lg6XW/O' + b'rjSaqC/X0baVkFOe0B2kg5t/9Aqc8jxcwfl1EBF5n5ufqimwnhkQv8+uO7HXAgACvnt4/+Gv' + b'fPXBB9O6mk0h1wwAgHfA9R5yxEiRLU7WSMwiCuCTkTjsUrtdWcCjjz3x+Ut3TLzEWV5hXwYC' + b'Gotn8a3vPTL9nZMLCgBKTnK7Zk720b2wTd2jDs7un1XH4IhbqXMWeaaWDASJMnBM64jqFf2w' + b'OQwhN6Ci9T/+9YMPDt/755BrBiAd2YPaHxxyhEgD0AwpCZS+gGkitKUSE2D7RgE+dWLpdw/P' + b'jL8TUGNQVklXkn3JhKrWqqQVDLmojAwEhgE4QvfPe9E/xp/t/U42aAivV59XRe8olnC0vcYF' + b'jI79D+ScAYheJdzW54q77D9iA2IDRdqmAWu7J2FWHrqQpkp57hK1KXy91frO6Vb3E/O1ygtj' + b'+9z6ZxTahrFzosZP1BvWHQOrCaiEdAfW1tRWS+PBgXue9/oB/HHv/A+JQOI7fKTdvG8gF7pB' + b'5JoBeEnPEvbzdpbXDEhSxLSCdp7CLEwAcQT6arVtygAA4Etnl9976965F5Jzicv27jYZhjFZ' + b'qzLXm4oYIMXWL9PPebfBe7jRW51NTkDEBKIIgaQm+63L7K341y8utEZqG/hcMwBAiJ59TziK' + b'YjK+Oy/INm0geJU01WtAPOHkTZLONq0G+OappY89c/fMd8pKX6I0g1iHHd6tWqAnKyVeaXeV' + b'YQXNtkrzfDEsHst9njPgw/6MoAXEtSFj1dInMWItBHPNAEIpqPBht320u/HGVWKRBjRsR2Bb' + b'DJxRJiDY+x22sQKAh4DmQ0vN3790ZvxnmRTKOr7njLZhzI6PYam1DJBN7S3BpvJuhA8M+972' + b'MABOOAPjKJTzCRknHD5ztjES6b8x9Lk/Mtro8bpy7JhhXy1mqTPaz41SHpIBaEICEDGwTRUA' + b'AMBnl5Y/SMxNZnGAAfAOWMZYpaysW9bVa/gkKjGpekds4iXf28hQF/h35/ouyQD0xxBZMhx1' + b'pwLaU+XaP6R4i1NBrjUAW1nFoSlIVHDBYFueaSIfgCSJp14owl7dkyhAfveKSQdfWWw9+Jxd' + b'3fvmKpW72GXAKd+MRaHRNZibqPHJektpEEhrAOlt2tIPaX71ajMg0gKcryNuVqLB//wvS8sj' + b'1+sx1xpAbGNZLtsrRWTzSNlI0niJTemO6HulUQXneLuotPCvC/X3EiQj00p5z4OZMVmruEo/' + b'uLmBq6XAxgf1GX0+l9QiNjvQ53l8zAsDZjAUpifH/naIU7Emcq0B2E04EYifZX9Al5dPlkDt' + b'hiHOVsuoIYjPP5AS1fRPkTt889TyR54xP/3dstKH4CMC8NVzUEqNlzU3OkZ1XFu3UgbFGskY' + b'fppgfwIVdABnzsQRgH88sTAy6b8xcs0AgsSPbe9wzHZosbsCgWyyCUs6cIqQrkMGYgLYpbCN' + b'XQAAgEeAxiP1zh9cPFn7KQaLJeYCAQptQ5gdq2K5vWIzAUkB2qbPqvM01db7JJ/XN50fOHoi' + b'z30/CvhGMWcbze7Q23/1Q64ZQO+Nht8aykcBWMKCTvorG3KiDOQBIxQnUaEBeDywtPiBAxO7' + b'flwzykZZaxgIZbvjlTK0snOlldvExTsCNk66630yQ7dCH4S0X3JaYQn45ENAc6CXsUHk2gcQ' + b'9//36cBRg0jJzZeuvaIdpA3fo5457AtQqAAAgG8ttf9toWvus6nZgShEMW8Qqx21KgvDNhz2' + b'3Ivt6NUDqweCMBjU4OjcluhtL4LYl7Fnx9TIhf8EudYAhPjJ2fVC+ELohnwHVsC47aUVVmWc' + b'pXEhdhFEizPdM+Qa31hovO+G+cnbDQCwQgmAJP4yMyZrVZxotGD9ADaDRswE+8l+kDkcrHzv' + b'h3g1ifbXhfcFmfueOP33w7q2cyH3DEAkCjkHk08BduyXmFyjCPsHrmdE6hfSwwAgPolCBQCA' + b'T55Z/Iunzk0cr0DtA9k92uJkLNZQtVKJm8YoYoJh7ZgER//2I/Vzu/eydAAK4u9n0QaCEHj0' + b'y/XOlzO+hAtGrk2AoOoHr6sPCTKja8hrAcRsM/Wwhvq4iRG+NyQFdaloCx6h+Vir84dddo08' + b'fdNUy5hbxNgxVk2o9i6OjkizQ09izWgOFcX/Gahq9bcYYYUw9xqAXxjs2i3GHJjItQKzzSS0' + b'fC7tjUHkfGzNjsIJuBpfWFj6wN7dc29jqJJidraYBTOjWi1DAegyQ7E0cO3fWi1OK/bHfAWe' + b'fCb5iezh1wFs9iMYWFD6IwO8hPNGvhkA90p9SBtmr4aH94gsd9ZAZi3BfFISZ+FqzDceXG5/' + b'5dgc/cN0Wd/WZQCIC4AUmkxqplalM822ViztNlTqU5UVghBwgsAeaz3Waf3TsK9tPWwRE0Ce' + b'h2YgIpHDcNtIXWjq1zojRCEoel6wgCQeXGm9jxjOLHP1ErDS2xAwUSsrmzkoGhT7DLv1huTj' + b'D3PIxh/SB9B1iPrGFxZa30n5NqaKXGsA3l6UZpBwW4ORtGAmGDgidTno2rqe070QxwuM9zMM' + b'VvXMCx46tfhnRyZrT5SV2uOLZ6KuP1prNe6cgZrYdQxixB2clO+3416r1fd6EI6/fmDHuMQx' + b'PVGrfBRoDOFKNo5cMwAr/W16Lyn4feGFAxMZmwOglIsQuOagGawO6QcQ9q4HtmlT4DVxHFh5' + b'rN39w32VyttEdJeEghloEGGmVkGjbmDAvusvmH0/wNi4Str9wPCIH04DkNJfMFAvl0c2/i/I' + b'vQkgbZfZER98WDDE5KUYKDjp0k4IkUiDbBM+wm7fIeOrS+3fJuaOTfpxm7c6HZ5ZoVouK8Bu' + b'oCHEJNpbD/r4BobpLrBrLk4Q4oXjS+3PDvGSNoRcawCAEJ9tKGE3hXBppWyrywwzZEtWm1zC' + b'qScCiZ0qDKkoBlob367Xv3jjjrEHxlndrKCgJEqjbBSnycBstcJnWh3rD1Ci9Eeqv1ot6Ycm' + b'+R3E9hfBA6jPf2Z5+eQQL2lDyLUGEFKBQ/tl3yUYIQwok2LALt0U6Q442180AXK9CAv0xbeW' + b'2+8nZnSI0AEFs00BXSaMV20akDBz8bcKks090mz2cSFDrs/XngDYOTk20uE/Qa41AB8G9E7A' + b'MC++8w/b0J9IZF7lRkoHySQkmw5aOAH64cGFpQ9fOTX/nxXUblBvXgZDgbRSVa25RaRE+S+B' + b'fRXnKEl+IMw3OUHEDPz9icW/GfJlbQj51wC8zbX6UcJN3pbMIAQYhxaDP8BqBgX64wywcKLD' + b'91pHbWgZphxzbhJjplbu8eX05HxE95xHZIRrAxj8vU63O7LpvzHyrQEgJN+4ht+9E+E685DP' + b'HbNe5NSdRRzZ/mL/c8EB1sPXllsf2LVj7C0GXAITyl4vs3NUKZcAhMxOQvDzCEblDss67MKa' + b'oSWFf35k1ON/DvlmACRmgI3/+1RgDhWBxAkfssqkGNBpHiEDsdAA1sfDKyufbcxUP11T6iYX' + b'EQyZgcyoA2qmWuaFdldJDYDi0VNZ47kXE7Cu9F8N+7o2ilwzAEAy/5xc58g3w+z3BaDo81aI' + b'ZNcUVFqDF/R/bny70f2tJ09UbpLUWQ9l6+nHKmWcbXVtbj2c8B/BG2udy97+5+82un837Gva' + b'KEaNoZ4XYhvRuEVk7X4bX+569TFyDkZ/k9bg+DwczlO4ANfHQ3r5TwzxKfHVdJldv0ZnDiil' + b'KlpxV7QrFmY/VKd/z/A5KJDsU3z9W+32tzO8baki5xqAnQDNCqRszzlZICEl2JkGzgiwm1Bm' + b'oAEgMj14JAXVyOHMGSyc2o0Pz5fwRhC7PgHhztUNYaZaUSeabcRmQBZRnAuFFyru+URJ/T2s' + b'OyAXyLcG4KVvILrYQS/ef8eZXUsqyRdIccTfLecanTU60nhwufWbLBqcaFQAhMxL5RIpxO8H' + b'ghuFIVqo7P77xWY7F/F/Qb4ZAALR+15w6E0Ntr6A8GbfPvKbHPCPHDGgggNsBN9tND7bJPyL' + b'bOQqff0BAApYIdKT5TKHpCvZEGb4w0Z+guZHjPZMZWzkdv9ZD/k2AdyNVwrQogGAvVe260wA' + b'v2mjko0p0u8HELeB8rsDF06AjYAe6ZrfvrRSeoYwzSCVFAwTxislLHS6EGeh20Mo012ENgIp' + b'+gp5DPzZL9frjw/3qs4PudYAAGdrc6yOudwAhHwAUHAc+T9KcXD8tc4WKeT/xvEQlf7IMC2E' + b'JCprWsl9ZOcMtF724AxM25l73s5fBEcgAZiuVj4x1Bt5Acg1AxD1327IERZO8Mi6/H8E51z6' + b'lYCIvtd5s2EXRYGNYWFh4cwCqT/1c8jBsw4Glg1hulJWzEBX5hvDV/9jE4AZ+Pxis2AAg4Rv' + b'vhAlYUTlmFFGYEjXFB9BmsPvCMTsNQ/iwgI4H3yz3v6tyJaGz6Rw7QJKJc0s95sjXwuGNCKm' + b'75K/lqbmzUju/rMecu0DcIqi58gqkhyMkA8gtqL0oU/bdhSnlWge4owsWMDG8b1m8/4jE5XP' + b'VYCnAQBY2oHYm7tCrCbLJV7uGiUmwLCdAEGgAAr8D185geWhXtAFINcaABCcfj3SHk4LoKS6' + b'npXY4Mg2DSphgfMCP9al37FzF7oruy4O6BCjViqBETLvJO36Qgq4NjNCG/pg6jWgRr77Tz/k' + b'mgF4Qkcv8YvKbxDqAWLzILMh5/a6SYHzwcMo/T4TL/c4csF+R2HSSpWUYunBEDSvwQ5glQ+A' + b'n+jyJ7O7M9kh1yYAYAnOkLT7AcBinzFIuvQiCH5CBq2jnCoYmE9YKAU2jsXFxdNLc1MfngS/' + b'WqjN7bwNMFAnwnS5pE61upb44D4zBEsgzvok5uMPdzpfGPxVbB651wBimz9s0mkJkSjK2uJe' + b'B06qA0EbiTPVCpw/Hm50328YZGT+WMwpu+mm1pqBqGcgMrLq1hkhm9S+Lmv9d0BvPVNekG8G' + b'4NXuBCEi1AP0euZTTgGOB3s25M9V4PzxSLP5yS7jK9JfIdRV2scVZjVZ0iwEKI+DgtckIzOl' + b'rVSu0n9j5JoBAMHu90QvahmF5+w4dk89QIrD+O8MUQDmIgZwoXjCmN+OtbmYmXaYUStrMHq3' + b'Ezcpz+nafp5oj0nLfLqPGc5V+m+MXDMAScGMF4rVAoIkjlU3j7R1wj5fW+DCcZr0HxBz3cf8' + b'Ye+p2PpGKVVCJIWZYYsx0p7YPkMczOHc33i41fpW9nclG+SaAQC82v7j2CTg8Bn3Zg+DSOk/' + b'QBZhIP6guhY4XzxRrz++ovRfSmZgb5UgsEyMyXKZvScewc8ziEhP7HcYK+mPD+cupYNcMwAG' + b'eir/fFZWfNx/Ej02Y7ojrjWI2UKBC8UTzO9lT3jx/QUARrlky4VFEktb+Czlv2R9GscIGIAB' + b'/jrbO5Etcs0AgGDfx9I/dJGNF4f8QfojDjEWiUDp4KGF+t93wV+Ni2+EAzCAZWY1UVIsNnkv' + b'g8gWYv8zuH5Stx8Y0GkzQa4ZgPT7C0lAkoQTGIP9ICJtIMsLguc0DBRewM3BLJZKH7RmQO98' + b'KlhnYLWkI9WfV62D1P9zAsXzfubPf2MZI7/7z3rINQOwEJU/aABy1JcDJz6XdgjQn989R6EB' + b'pILjTfP7zFhmDjsECRQA1kqVAJZqUO8PSNPmj0f8/QB2VEsfG8JtSRW5ZgAMUbcpaAOIk4H6' + b'OQYzGtF3i31YYHM43Wg82lTqIz73380rADAUlgxjsqzBbEuwpVNQFvDCQ66Bga812wUDGCqo' + b'l+D8Ft0IjKCXULMh/55oA2QRFiwgDXyvbX4z5AQIk7ctnhgMrTXHZkAUn091cELbM8AJ7uLz' + b'g7sT2SDfDADwan5PDgCjRyMIJJqRFsARuXNgNQU2jydarfu6rL4uyT5S/Sc5ASvMelwrHxIU' + b'9TxtiIkpWkaZcf9xYCWDUw0UuWYAQfpzZA4It3ZkGBMn+pgEaQz0GQUHSAvtBcbv9ajgkDlV' + b'6DBQK2mv8Un4l9aalwsYsq5sBaJdbXvGS7nZ/Wc95JoBxMp2D+EzfHdZ3ysQkamQ0UgWHhVB' + b'gHRwhtXvEaPhid9xXSkCJAWl4ZyBiIQCpzTAPeuIAf5co5XL8t8kcs0ALOFH4R95BMAgGJH9' + b'SbGf+oWs/u5CAUgPpxuNRzq69BFitlu+w91fd8uXiDFZ0srX5yMjBs/eFPjW4x18fUA/P1Pk' + b'mwFw0lEjExXCcYiOy4LJxASIzhU/FkgHx1ut9wEIDV4BsPItIKC0Yq+FiTBASoMj4gdQVvgk' + b'crT7z3rIdUOQkJPNYBVtDhpNWOwQkp4hae8ODPSoh5k4obY7TrbN3+wvl76pwZcb1yFEqcAB' + b'Ggw1rhU3ia0mALtl3KabhXhGEhy7rDnX6b8xcq0BAGLrwU2U2GzyZsTHufev0hyxpIgz0Qqk' + b'is4i4UM+wsPwZcKKgTYDVaWi7d96NbMLhWXorvMzAURon2jSP8xQ8hwAABBASURBVG3yt4wM' + b'cs0AeuwzxGpflAzU57NZjZ5rK+g/dZyF/iAB7dUFWBZGKaUR9epPMugLGhyYOwDF/KXvAd8d' + b'xO8dBHLNAIAo3CMqPyOyzdk/B1K2C5PDLzis5gYFUsHZZvOhttYfswweoUDIqfl1YkyWFIvX' + b'PnLabcq/4/sOgDFeVp8Y3h1IH7lmAJ5DeyYQ1wUEKqTos9lQZ/j2WP0skD6WSL0vLv4J3Zdd' + b'azCt3DxwtIPQhf+XLPn+dtfkPv03Rq4ZABBJ+kjhC2pb6BfH8edTHuDo+xOPBdLF9xqNvzLM' + b'D0mzEJkDBQAMNJnVmM8M7DUDzxdBs/SCZXna4F/S+i2jgHwzgH4qfeJYPP2ZxYd7zhfrAwUy' + b'QKdTrX0oZH+GO60U0GLrDBRtzH8Om5hbt6Y0+IGHgLOD+6nZI98MAElpHBw2RCEisJo4sx8F' + b'ssOJdtc6AyMNL97twSilFOJNY2GFxQWMOPW4CbVlwn+CXDMAIbSeZKCI+norA7MldmE8RRAw' + b'eyy2Wt8krf9OpHzcGlwBqDNjQttsD7ul+PlrAHFymXz3Upf+dhC/b5DINQMAZHLYU2JYFFgt' + b'jjl+kt7gnu92l1NkA2WKJZR+0xJnVCUIgKFAsBuIBEKO3XobZeq95eQEHJ8DvjTQHzkA5JoB' + b'sBO7IeQXT7EcC1qBSOnUa8Vj7YLD6wLZ4bFG46+I8V17z8OsS2rwCrOu2s2EfCPP85kTkf5S' + b'/qvBn3oQaKX7K4aPfDOAfhxdiDFyDsVqHSP5B5sf/UyCApmj0a7U/sCnfXOYbZsZyDYzkIMp' + b'uFobWCv0FxyHkldCauvZ/0DOGQAQlPCkFO6x+SItINNrSTCDohw4W5zqdD5I4Jafc7YmAODm' + b'wJUCiFnYowmuM4BVa4YXOnTfIH7ToLE1GICYAO5ImMTYGedZRdoKQOJbC01gUFhut79mtP6U' + b'36jTJWVI0dcSA5NxlSDgewquh9WhZHz9BPCtTH7EkJFrBrDKsxtL/sjmS2oCm8kM6/cfuHfR' + b'FBgcmqr8XkvgEWNXgOwmDLW6UctGIkJxNWkF/HfYokWeuWYAQJDywfa3BN5bHLSaq6c5eirP' + b'Cg4wUDzWaPxvJjzK3N/R1wKUOAP9HGGd+eTeeQUAVdK53f33XMg1A0hOmsx+8AcNXiWPGUKB' + b'gaBuqtU/ElNMQnfKzUCTgZpkBqJ3zfRDH+2xfbpt/jHj3zA05JoBxAi2uHvN6xBi2ipA4qux' + b'+nCBDHG6a95PQLenGjRyBhr3OdfOO+wtwP1HnASkGJ95HHhiwD9pYMg1A4i9//Z1ksP3pgOn' + b'Hf9PZosFI5F7qhELZIvldvurrPSnwtzbZ+IMXAbUuFK8an2swczjeR0v8ZYq/00i1wwACJLf' + b'Z215go8885HdF/4mIyWAA/MpMDjUdel9QMIf48CwhUI+ZwB95g295ptoA492+G8GcPlDQ64Z' + b'QJIAQxmwe82xabCGzp7ilYQkkqIWYNCoNpt/ZpgftyXg3DvTzGgxq4pzBiabyK4zzi4Aud79' + b'91zINQMAep02sYe3d4I5ew+9//7E4iswEBwHVlB2zkCGN8G8MxBAVUERog1EEmKhl4EzNPM/' + b'Ygvs/rMecs8AkqTG/t/+Ujg7+V9g2DhjzPsBcCzh4zJhUoolMzAIibBi5FGchPMV9dFBXPcw' + b'kXsG4OP+kRYQnDy9ZLnp/nDrDRRMYNiodzpfIq0+5YnbhwTd+8zeGZhMBgLYV5C6eaRvtmlL' + b'7P6zHvLPABAmMFji0eR6s4B7Pp/NiNT/ghsMBW1V/gAQJLzMg5LXKqkBhH4CCd/AowtbsPw3' + b'iVwzAK+2JXwA8oYcj7WDTDkAgpZBGEgxkNrA2FY40Wrdy8CpmCnHaAOqDFA8T4KYKWjGfdgi' + b'u/+sh1wzAKC/DTcsdTzDc6ro8XyJe7sxgTqVy38UqgMtpGlog4GaUiom9j58HKyGvvuPhr1s' + b'ecyEueebAcT2NzsV3D/2C8f15gikVgyE1ZImBSQneiPSfrsRe18sGP4AXCvAVUwAgEmUCfue' + b'AcEv0FogDHr3HyF2DaAUPe83t6nJuFwzgITG33O8390R4k8/G7CX/C9wZuKJ1onn/RbDWgtj' + b'9U/eZljpdD5LWn16lWR3WwmuMDCm+luEDv+2MJjyXwVL7OXEKCHM6zqXuXnkmgEA66v+4hPo' + b'DQtldx2rM8zOaUImCV4nRqnPsZgZyKnjR6zxeluhpfX7g65n4TUAMOB6B/YbYwofz+iyYoKv' + b'RqPijsm8rmWhpI4twQCSFJCsDxjENfjnHB1bTf/9pHoJYVEkpUEpGkmi76d0DNMFMlI40+r8' + b'CQGn4z4A8U1pM6tKH7+whU7L/pf5FoKvuTEOYMwdK6OX6LuA39Qo88LS3DMAAIB0/uHVlIDE' + b'67TuZjAnIpOCV31/P6kuhF1ZZ8SE38/2kxEvkG1P9AmcRaXyP4HgGwLCzXSZgWAg2kIMALBc' + b'N+YzmzivzHkFlsAnojEJS/yV6FK6iTEQwheUB3GSrNBL1Fat63X8Rc/t2/7p5s6aYDQJxiOh' + b'JQqSPCn513PcxYSdLDbsa+ls6udsYSx26f0zwOsBlERAAICCAisG2RKhnhuqgM89Bpw4z1PJ' + b'3IqmVkZg5GtJ+I57jPnPwOcy1wxgPSRNAwkDAVhNcuf8stVEH5+jnzgGAF3COAwmE2eNGQAS' + b'fx4TvMFqRlBI+/NAo9t9YLqkPw/wUQJDQ/Xc+DozxgFeiVYEY8PhP5nH2GSroteMkzmLJXwn' + b'et5v2QwUuWcAnioclcfdgQi9No7c7Y3Qf7Kefy1zop/JwQxoBTBKs4CZQy8hxx8XIjeJIccK' + b'ib9JdJR+f5XMUTHZlAsAKrY3WCOsF6WABq3pAIy1OCHyCqxNL468WLU3sETeAtB2Q4h/oGr+' + b'esg1A4ipSSE44OSY+AQSKt6a4HUkvTyuPsY951VaLTcVPrdI/PGzhhYB7II1Oduwi0IeRQqY' + b'6DGpDhYSf5MwExN/zMtL7wGwQ5iAQAHo2MxA7tqXj00AX17q/YrYWStEPuaGOPVK8Slhid7A' + b'zrvMfWzjj8x85poBAD2mPWJ7rp/EVlh959eS9GtRoLCInj/Tqr1CeGiB6EtLXfo8gNMAFgFM' + b'IUjzFqwEiCVB0vFTOPVSxuLi4umxavXP0e3cI7UBKpICKwBmAFUHoBkPHLeHYntenHk1WEfe' + b'GIIXX+x7se0bsPNcj54L8Y8U4QvyzQAchbJa486y1QpUQuyfD9GLky95BqWUqQNnFgx/b7lr' + b'Hma7bfRZBC+/lAM0YBfECnqlwVoOoJFbJHnHEtF7p4B7gN4CIX/jFUgDmsEfAXt1vgZL5OK9' + b'F0/+OELMvgs7p0tuLMLOcwNB04tNv5FDvhkAnGR3TMDXenOQ1EErUJ6yYk2gP9HzanMCgFaK' + b'loGVReKzy11zhi1h12E5/QosAzgF60U+icAUhAHE0qCQ9gNCo9t9YErrzwH8tH7OlDpDTwP8' + b'BOGzAPbAEvksrAY3DcsAarCMgWGJfhHAAux89yP8kSX6GLlmABuhmphBrGUaWF8B96VGpRTV' + b'Gc1l5uWlrllmS+wNAMuwC+AM7CJ4AoHwT8ESviwMkfzJcF6BwYC6Ff1bZcM/CrckHHUqBlQX' + b'0Fqpaoe6ZQDXwBL/HKzEr8BK+xYCgxcmL/Mr5t1IqvnrIa/FIwoAppR6cEaryxSs1x1w+hZb' + b'HXxsrMZotZQPuEe2gO8ajNUzphRoidFaYdQXDdXZEm8TVpIvw068SPeTsDb/Kfd4BlYdXHZ/' + b'00JQ+QvCHw4kXCdOvCqAiUqlMt3pdGYB7K4AOzvADCzhj7u/EaIXxv4Y7PwuItj4I+PRvxDk' + b'UQOIEmik44tKOANXS3QFQMW7PSa/VCleYm41GPWFLq04SS+qfR3BxluEJfSzsIvhNKwmsOA+' + b's4Kg7kuyRyH1Bw9ZJ3Huvaj2cwB2dTqd3QB2A9jdsap+GXbezgA4Dkv4jyNI+yVYph6bcblG' + b'3hhAv4y6oOYj8tIn/rAf5SmleJm5vcKoL3TNCod47QqsBBdpLxL/DCyhn3WPsijE/hNVP5nl' + b'tdYlFEgX8fqI4/TTAOYB7ASwH9bO3wXLCGqwBL0E4GEA34OV9I/DzvMyVtv2W2Yu88QAknXx' + b'ZbZ0D4BBPcHAROZfdFwr8DKjvcJoOKKXUI146+sI0vxsYqxF9BLeE8mQ9P5umQUzooiFgoTs' + b'xgHsgCX8iwDsgyX6eVjvvoEl7kdgCf4JWKI/BTvPSaftltTg8sQAgF6VbkyJVh9Re0/EzvEE' + b'rYBlVu0Gc+NsUO9juz4melHxRdKfxeoQTz9Jn0zdFWy5RTNCkPVQQgjZTcOq+UL4uxHseiD4' + b'aE7D2vVnYFX82G+zbXw2eWMAQvzjAGYY6BDQhNsFSkX+AQ2oJpRZgVo50zENDp7a2Jknnvx+' + b'Kr6o/mLTi09A8rmTJZtJ1XBLL5whIrbtJRNvElba74BV83fCOvTG3ecWYW36s+hl7uLMa2C1' + b'v2ZbIC9RgFi9E9VuN4CLARyEVe+E28/CLggpzJDKKwndiV0fS3gZot5L3F6IPs7c65enXxB+' + b'9ohte1HxRdrvgJXyU7CagIadIyFuMd9Eyot6n7Trt93cjbIGENv8koctxL8T1pGzA5bYZdLF' + b'gdd2fye592Lfi7Q/gyDtFxAYgyyYWNL3S9UtiH5wiPPwJTtvGlbCz7rnwuwbsPMpOfiiwYmZ' + b'lyzI2ZZEH2MUGUBcsRlXXo3BcngZNfeZJiwxd2Anv+qOM0KqZh1B6stjLA0kXt9B/zrtfotl' + b'Wy+cAUCkvbTNklTcKYTMvDJC7n3szI1z8kXSj0T57ahhlBhAsimmOHeSFViSjtmCJeY2LDHL' + b'gtAIxRmiEYgEWEavei8e/KQzT4i932IpFk+2iOvp4/ZZEwhlt3ElZQdhLlvo9dWMVOntKGLY' + b'PoBYze/XIy/O3hKHTzV6jDuvSPsskfyi7ok6GC+SfpI+lvLJBI9i8WSL2L6P4/cypMBKyvdl' + b'3iQaI0QvpdWFer9BDJoBqMTzZM+8fo0x4+aY67XUkrJMGULgsS2/HtEXKv7gkWywISp/sjei' + b'zK3MpzB3ceIlNbcCG8QgGUCSWJOtr/t1wY074QKriTXZYSf53lqjIPrhI55naYst0l/WBxCI' + b'O2bgcTk1UMzbBSNrBpCU+P363ycJXkef70fQogKeq44+SeAF0Y8G4jlPtkOP+yTGhC+Pazlk' + b'C1wgsmQA/Zpg9mMA/VT5JOHLgkguABkqekyi32IpFs9g0c+5G6v3YtsLk0/2xi9SqjNCmgwg' + b'Ke3lMSb85Ov4s/0kddIxt5a03wiKxTN4xFl78hgz/uRcJ235Ys4yRloMoJ9zT57rxPHkOddT' + b'0ftx/mJRjD7iuY53NZLRb7+DIlQ3BGyWAawn9ZPHkkhK8372fLEg8oP1tD5BP82umOMhYjMM' + b'oB/x9/vO5AI4n+cF8oEk8a/lzykIfsRwIQyg399s9Hv6OeNUn+MF8oN+BC8oCH7EkRYDOB8U' + b'C2LrYK21UMxxTnChxLyRvysWQYECBQoUKFCgQIECBQoUKFCgQIECBQoUKDBM/P9EbuQz0a4x' + b'6wAAAABJRU5ErkJggg==') + + +IPconvPNG = PyEmbeddedImage( + b'iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAmN0lEQVR42u2dB3wcxbnAZ7Zc' + b'P/VqWZJtyZbcOBeMbSBgjAnNjxrAQEzHlIDoCRBCEhJaCCEOyeMhCPBCKIYQHiV5AUIJjxYw' + b'RoAxRrhItiwXWV0nXdud930zu6eTYuNbSSvdEb7fb3XS2dLNfPOfr83sLCXfyL+10LFuwDcy' + b'tvINACMsr936g8KOpg0XmT9LGTn0W4cteXPT1m1vzrvwam2s2zdYvgFghOWJH3wvcMyCeXXm' + b'z1JmNtMlB5U83uZN6z58pqy0fNXGzQ3v7X9BasDwDQAjLE8CAEclApABAFCFUslQNaVEcnka' + b'N61b86fSkpJVGxubPgTLoI9Ve8cUgNCHb1FZohczRmSGjZFliTlcf3ZOndU0lu0ajnAA5s8d' + b'aAGoSqFvMPaUMOgsfCFMhzGXJCI5nBvqPnr/aX9O4VMlOVkfFxx5EhvN9o4pALtefeGwvILC' + b'13BWoFKIy8M0TbtArQ48NJbtGo48feMVgSX7BwZYAOCbEkUAgMLMr7qAgb9KMqOKvL5hd9tT' + b'erjvqdmnn79uNNo7pgBseu7JJyZMmrSMoHnUQAm+DLZ5a9OqikOXnD6W7RqOPH1jTeDwObPi' + b'AMhoAXDowQLgjB8oTNCAFoH1f48wSA71k7a23U/rfcFVFctWbLCrvWMGwOfPPp43pbxsC1Ed' + b'bt4KqjDqcIECtJZ/fvxJ8cJTzhwzvzgcefoGAGDuQAugIwFoAaR9qJsbBMMqwKXDL4KL+CJ7' + b'8dJqu9o7ZgB8+cwfr55YUXE3BAHgDxmhbh+jjPH21G/ZMrf6iGPXjFXbhiNPXX95YEkiAJlZ' + b'TIsxKikKDwCtCPhDZKI+96iTq+xq75gA0P3O32koGFybnVcwjZt/nCIwU2gsytuzYWvT96cs' + b'OeausWjbcGXV9ZcFlswxAKBoAQCAqA4AJGEBEgVDA02HF1qfd8x3vl4ArH/qkW9VTJr4JlFU' + b'Ye6cbiZLECihguDHTU1NL1cefsyRY9G24coqyAIOn71fnalZGcCORWPCBVAp6b/DA0VuAWh9' + b'/tLTvl4AfPD73/5h1qz9lnOTiOYfZ7+uUZ4mcfRJ7+ebN+fNOObEvrFo33Dkye9fCgDMrOOq' + b'pQYA4QgAoFi0AKAJjSfH9fnHn/71AWDL80/mFObmbKVOp4e/IalM9vqgwzqNp4MAxa72jiOK' + b'Dzrs76PdvuHKk9deIgAwNCv5EYAwtwCSZMECMCMGoFJ94Ynf/foA8Pmj99dMmjx5pRn8Sd4M' + b'JuHUx9nBgyTGU8LXPlh95xHnrLh+tNs3XHnimosNCyBEhhgg2hcysoDkARAWQEed1Bd+55yv' + b'DwC9rzz7seT370fQH0Lwp+TkQ/AXMwAgwvkBGG1dXWvyFx42d7TbN1x5/OqLAotnTa8TioVu' + b'+bNYrK+XUlWJF4KSFW4BmFRfvOz8rwcAnz5078KKysq3QRno7Al1eZiiqBQLJDQBAF4m1Vns' + b'5ffeH3fMistaRrONw5XHr7wwcFgAAODxjGEBgkEqqdaCQBQ9poFOpPpxZ6z4egDwzsrbfj9r' + b'1uzzhPmH2Z+VxySicwC4+Y9bAA4Aier6ae7ZC58azTYOVx6rOT9wGLcAJgCZLNoTNNJAKwAw' + b'AEC4gJLll6Y/AB898OvMyeNLmiS3x8c/FYI/NTMb7RzlijFbYlbDAII16+sfnL/s7AtHq40j' + b'IX+8/LzAYftNrTNTWswCIl2dYAEUizGAWQii9aXn1qQ/AB/fe/slFdVT/5NgTRyXR0AxiozB' + b'H9bI6cCmMF1YAUIaQuHIJP+Cw0Z1hWw48uhl5wQWzZxaJ8w94y4g0gEAOKy6AGEBMAsov+Cq' + b'9Aeg4eF7P8wtKZmDwR+af2fROFH5k/61Qsa4G9AQAvblth3V048/tX602jlc+e9Lzw4snlld' + b'J1JaEQOEO9p5DGA1DdQ13DMi1U+4+Lr0BuCDX/543uQp1e9Tp4P3TPL4mNPtFnl/PP0b1Hu8' + b'IA1a88WXly5YfuF9o9HOkZD/vnh5YNGMqjqzTzJkAZH2NpEFyNZKwSwm6gCTvndDegPw+s1X' + b'3T9r3gErePAHg+oqKGYyBn8y+v69zAp0Axojm7fvfLbqhGUnjUY7R0IeXnFmYNH0qjrTqsn+' + b'TBZu3Q0uAGMAOem/w3gdAC0Ara+44ub0BeCdO3/kn1xS3CT7MzLEjFeYp6CQYOmXA7C3JhiB' + b'INNZe31Tc+F+p54VtbutIyEPXXB6YNG0KXV8tuPSPgLQ0kJlAIBYzgKEC5h87S3pC8Dfrrvo' + b'grkHLHhAwtkPClFz8phThdxfkURhhO4dAF4PACvQ0t2zsPTok96zu60jIb8/77TAIdMm15n+' + b'XvJlsNCunQCAank1UDMsQPX1t6cvAOvuuPGfeRMnHYApEA6or3QCo5omNknuQyFYKsY4YFPz' + b'9punLzv3Z3a3dSTkwXNOCRxSXVlHZQGADAD07dxuAGB1LSAGHEj10266Kz0BeOO6i2ZPmT79' + b'Q8np5J+j+DKZ158hyr5m8WfvKhB75QCa9u7gm+OWnnKonW0dKak962QAoKLOtHgcgB3N3AVY' + b'XQ4WQSCtn/GTX6cnAI+dfcpvFy0+5Hu4Hw6DP19JGVNx5ccM/vb16Tozy8Lh+m3b8meddXG3' + b'ne0dCbn/uycGvjVlYh1W/kwAepubqOxEAJIPAhEBLSZcQODW36UfAC9fe4m3srCgyZGbnYUz' + b'ncoqyyotx+SWW4BkF0ZMN7C7q+vY8pOX/9Wu9o6U3Hf6cYGDAQBZEYMtef0suK2JKg7rlUAt' + b'FuNrAbPvvD/9AHjpnNPPqZo/62HcCIEpnTu/mHk8HjH7B1f+vkq4FdDIG3VrVy79/s1X2tXe' + b'kZLfnbY0cPDkCXUy5P2EL3f7WLBpC1gA1VIhCM0HWgCMAfb/1UPpB8A7l1/4dlHVxANxwHFv' + b'W97kqUzCXT+G+U/GAPD6L08FsSxM1/qPOnmmXe0dKfntKUcHDqoo5wBwF+D1sp6tW3gQaBkA' + b'Yy1g/m8eTS8A/ue7p8ycOrPqY9nt5EtiamY2y84vEFZfliytizNzY4Su63//6NOyk3506za7' + b'lDES8puTjwocNKk0DoDk8bLuLY1UhRjAaikYXQBagAPveyK9APjPY5bcs3jRgislCP5wb3v2' + b'pMnMpSrGzREWP9JYHkYrENX1s/NO+O4f7FLGSMjKE44IHBgHAIbP7WVdWxogBgALYLEUjC5A' + b'hxjgkNqn0weAZ849zV2ek73Vk5+dy2e6rJKiKVMZ1WOG+bf+kSIQ1EjdpsZHF11z01l2KWMk' + b'5J7jDg8snDC+DgdcAOBmnQ2bqWIxBmAJQeCih/8nfQC4/6jFZy5YEPijrIgZ4BtXxjIzM/jg' + b'033m/ntThlEX18m21Rs2ln37+ltS9q6hu5ceFphfXlLnUFW+w5m6XADAJrAADiLJQ3ABlNYv' + b'efQv6QPA86cd/4/SqvJDkHYM/koCc5lsbPqwtiNmoDZ0XhbWyaZdu2fOu/wHa+1SyHDlrmMO' + b'CcwvG1encgsAHs/pYh0cABXCHyuFIEZixmrgkU/8LT0A+P3Ri6dOq5q41uF1StBy4szOZUXj' + b'SynfIm8x+BugDGOrOFqBzS1tV8+74oZ77FLIcOXOIw8OzC8trkOTz10AArAZAOBBoIVCkJkG' + b'AgBHP/339ADgyROOuqu8Yvy1WATB2V9YNY15Xc5hmX9TGdwNgEI6Q+G/TL7wqqV2KWS4cscR' + b'BwbmjS+qU00AHE7WtnkjdTgdlgtBMcMF/Mez/0h9AB48bomzxO/bmpmflc8XfiSFTJo1h1Et' + b'Gj8cYTifxgwLANlA94aduwsOvv5nIbuUMhy57fAFgf1LCutwwBnTDQA2UdViHQBdgBaF/kq0' + b'/vjn30p9AFYtPfzUgvEFq8wKWFbZRJaflyMGH/3/MP8+M5aHsSbQ2tt36IyaG9+0SynDkZ8v' + b'OgAAKIhbAKoKC4AAyJaCQBED4H6Ak/733dQHYOWiBa/OmFy2GDsZg0GqmHsAc6ABQwBkC6Xf' + b'vWskbgUaWlpvPehHd9xkl1KGI7ccun9gblF+ncPlEAA4HKwVXQAPAq3tCBIuQKo/5aX3UxuA' + b'm+fNnDyvomy9y+uS0He5cvJY+YQJfA2IKonmf5gfBxYAd8p29Pa9N/O6ny60SynDkZ98a3Zg' + b'DgDgBBegGwC0bd4MFkCxaAFIHIBlr6xObQDuOHDO7TMnjrtegdmuwSCNnzaDZUDwx++GQeop' + b'TRj/oX+kWQ+AK/bml42Fy3/3UJtdihmq3HzQrMCcwlwjBkAXABagsQE4gCzA4rZwMw0847U1' + b'qQvA7QfNdhS6nQ35+dnF/HYoWSHT58yF4C9Gqbnn34z+4yAMIxvQQSlgBeCvnzT5mp8+a5di' + b'hio3LZwZmF2QW+cEF6DrJgCNAgALFqA/C5Drl7/xUeoCcOu8GSdVleQ/o0Lwhx3OLZ/IinJy' + b'+K1QUmLt3xh0Sgf+bEkMC4CbJd/Z2HjfWQ88dqldihmq/PCAGYFZBdl1LpeTuwCiqKx1SyOP' + b'ASSL6yBRYzn4nP/7JHUBuH5G5UtzJo37NgY4USB26vwDmVPX+m+EMD9hJKwArwiK3bIQEH5Z' + b'/aNfTLFLMUOVG/afGggAAE6nU7gAAGD31q3CAgylDiBJ9ee9tTY1AbigonTigrKCeq/bpWDe' + b'6s7OY1WVlTC24LlkuZ/4RN9vfG89JhB3h+maAEDXNPbJjpZJy2ofa7BLOUORH8ypDszKz+IA' + b'6HhAHLiAlq1bKK4NWCoFIwCaCAIvfHddagJw2ZTyn80dn3cTBn8xMM0TZ85iuT4PHvjJzf9e' + b'Z7rpDoiF4hATxREsrmAmgAslTd3BFUff+/ADdilnKHLd7CmBQC4A4FKNGEBlLU3bqFO1lgVg' + b'fyMxUQi6+L31qQfAJVNKlXyno6E021eCA6lJCpl/8MFMxsqfsq/KHyX9Y58EBMatofFjVjUA' + b'IBojn+3YteqMPzyzzC7lDEWuCVQCAJkiBsC6haKwXSYAliwA45MK9wNctro+9QA4u7zouFkF' + b'mc+pkkj9iisns8qSYpj9soh2E//y4Ht7E71B0gAwcdOosTKIAGgxrWVLd3DcyQ8/FbNLQVbl' + b'6v0qAjOz/QCAQxSuwALsamo2ALCwE4oYQSAAULNmQ+oBcHxxzosH52cdizM9DA096PDDmZ+n' + b'/ZIR7CTc8E/+lYH4hye5OZAZx6oyIxXElTKEoFNnc4+8/7GUOVTyyhkTBQBGIQgtwM5t2wUA' + b'FusAEeOQqKvrNqUWAAdm+0sX5vg2ZamKgp305hewQ+YEIOPD4E8xxnSQCWDx7wZ+eDKBIGMG' + b'ACy+JoAARKNRsq0reONpq1683S4FWZWaaRMCMwAAtxEDEFlhO5p3WLcA8KtRA4BrP9mcWgAc' + b'muP78fwMz08U6FAU/PHs+fNZeU6GMP9G6kcT//RAYxCHIDkA+k0/SwRAQwsQJZ2R6KvHPfb8' + b'ErsUZFUury7jALicAgAmywDATg6A1V3BUSMGuP6zxtQB4Oi8DFnW9I0TXGo5P9JHUclJxx7J' + b'5EiYr/tj6seXfsyBpSQOxJ4+bF+bRHjgZ7yaF8YAeHhCFOOAqNa3Ldibf94LrwXtUpIV+V5V' + b'aWBGlrc/CJQl1rxtF3Wp8hAKQdBPSut/+PnW1AHgmFx/tltRrpIJm+KTpGri8ZZPnlDqyPf5' + b'WI7fo+T6fRplulvHo9/0/q174kZg434A2n9nUFIADJr9vBgEAMQAACyWEFX99rFPvPiKXUqy' + b'IpdOGR+YlukdYAG2b99lxAAWg0C0ckSq//EXTakDwGC5cmalGopqRTkOZWpQZ1Peb2yurs7N' + b'KnV4PNOzMvx5OX6vUpTpl3J9XuZzKA49HFak+KlgdJ/3hw4M/nQ+++O1AC3G44B3trX88s4P' + b'Pr3OLiVZkYsrEQAPACBK40ySWfPOFupUFKJYzAKwDqCBBbilvjl1AfgquXVu9VVePfIrHHAc' + b'wAnT92NuLUbl+NkASSjCeJCCbtwboJsQ6MwIBGNEJ/SjU/7yxhw7+5KsrKgcF5iW4a5zO8Vi' + b'kC6BC9jRChZAthwERvh9AbT+5xu2pycAN0yfVDPOKa1UYMA1UEZJ9XTmjIYprhtIezobaI+K' + b'EKaf3yRhBH8mDJomsgF4T1/T2VN89+q1u+zsTzJy4aTiQHWGp84DLgDrIwwA2LazjccAyRaC' + b'mNHvCI8BSP3tG3ekJwDXTBlfU+5zrVQVmQ9WcdVU5giHuQWQkrxD2PT5sahGOiLRzY3dfYV5' + b'Xrec43bGqKZ5ouEIjYbDJEzY6Re9/v6TdvYnGTl/YlGg2u8BC6D2W4CWVuoCHSQbAxh1LxLS' + b'hAX4xaad6QnAVRXFNeUZ7pUOAwC8Q0gNhcRaQZJ3CKO/R3OPK42f7mx9pKG14w8aIVWvb946' + b'Jcfr0Ysy/Epuho8wp+udJz5aO+anip47oVBYAIfKF4M0ChZgV1s8C0hG4dwC6P0xwN0Nu9IX' + b'gDIAQDV2ChVWVjO1r7cfgCRdAJr8KFiAT1raan/+/qcXJfHRYybnVJYEqlyK4QIYzmC2raUd' + b'gsB+F7CPuDfe7zBmAUyq78wvWpZfVDRjs+SscuhRjyMaYSzYLauR0K4HPvj0tuG011YArpxU' + b'XFPq77cAhZVTmNQb5ADISQIgbpPW+faoT3a31962+rOUBuCsivGBKS6JWwCEXgcL0NzaQZ1y' + b'vwtIFoAIxjmyurtkwUIS6e7y97XsjPUF+1zdPUE52NtHunW99oXdXcPSh60A1EwqqinzuzgA' + b'uFO4oAIA6OnpByBJQUXGICD6tLWj9o4P16U0AGdOHAcAyHVuVeYWAIPA5tYusAASSfbmYDMG' + b'iGE1ELMdnAS6SImxuhwDfYThDdXpPOXR5t1/Gk57bQXgsomFNWU+dxyA/EmVjPZ0UwVdAE3e' + b'AuB6A66MfdbWVfuLjz5PaQCWTyiaM16lHyoS3yBNnKrKmtu6qAsfiZTk/RFmFiDujIf+4ysT' + b'g48wRECXvZoe65CV4pd3te8eTnttBeDSCYUDLED+hApGuhEAamlt3LQAn7V31f7y4y/GHIBr' + b'Z1bS5hjLnVxUOJlkZE7dTpWqspysMr/XU82isbJwV4db6+pi4Y42KdTWJnV3dDoceHsc2bcL' + b'QEkEQDw/Q6yHaPzSSZ8AYPWf27rnDbcvtgJwcXkBAAAWAG8WAXzzyicw1tlFsS5g1QJgDLCu' + b'vbv2nrUbxgSAK2ZW1PQozpmTCvKrqM9f5cnI8Ic7Oki4s1OOdnfG+nqCrt5gnxSLRXib+Yn4' + b'eBoePjXEeCJOsoo3XQAffNIPALeEcPWBLrp1dvuL7T03DrdftgKwAgHwueJBYE7pBKZ3dhgu' + b'IMlzgnjHdW5B1nf01K5ct3FMADivLP+xcq/rDMxe0Ld39IVZezBEEW7c/CZ4piSe3NKElG9P' + b'u+K+qs/GF2EBhOk3XQCuvvbiSepUPvyp3Z2vDbdftgJwQVm+AAD3DIIZzxlfxrQOBMDIiZME' + b'wLxPbn1nT+1v1zeMCQArJhUvGudUXkf3hX45HNNYU0cP+HaZKAll7cFd2tuDDpJzA+Y+SPFz' + b'3P/rek+UkYLn2nqG/Vg9WwE4rzS/ptTrXIkRMOiLAxBtb+cuwMrKmMb3x+mkvitYe98XjWMC' + b'wJnlhVKxKq3zqXIVmnQciF3dvQz6RR17qWoOdfATfz8eCxj+PwSxUKeuv/SX9uBRI9EvWwE4' + b'Z3xeTakPADAsQPa4UhZBACRqpIHJxcS8FAxXfVdvbe2GLWMWBJ5dmnfteI/zLhXMPprinnCU' + b'tYIbcJtZjQVJPhvotwKYFvIAUGfXvtDec/dI9MlWAJaX5NWUgQVw4GIQWICs4nEsDACokgiM' + b'ko8BBAAbevpqH9zYNGYALC3MKpiW4W50SLILBwUgADcQJCr0RE3mDui9uIl99V8AIGoCfVgW' + b'p2T2c63ddRb+zL6aZI+cWZJTU+pxgQWQuAXILEQA2uIWIJnKOEuwABt6QrWPNDSPaRp4eknu' + b'qlKP41TM89ENdPSGWDAUo05JMh4HsOc+/cu9MUmKaQVwEoSxAKSzHb2MlfytIzgiB2XZCsCy' + b'cQiA0wCAAQBFDPJiispTvkJZg1WAVTAEaGMwXPto4/YxBWB5ad6SfIfyCroBzAbCUY01dwWp' + b'SxJxzb/GAnTIg9+vAbEfog+uEKWPv9DafeZI9cdWAE4tRgAcPAvAYk5GfiHrQwCoJJ4Wk7QF' + b'QP+nk0294drHt+4cUwBOLMqW8pxqvU+RKnDAccFmV1cfWGhG1cStbgkqHgkAcAKFIP0D13nu' + b'n9p6Hhmp/tgKwMlF2RwANI8YwPhzCwQA6AIsnBmk68L/NfSFa1dtaxnzSuDJxdk3FDkdt2H0' + b'jws2QQgG2/vCYAVoPBgc8HUYg4/SHwAyPUzJxJc6gltGqi+2AnBiYVbNeADAhVGzjgDks97W' + b'VggCJUsAaDwG0EljKFr7dPPuMQdgcV5GcYXH2QBgO9BCAQRsR2eQABBUGWAFBlqDoSob/X8E' + b'D4xgZP1z7cGpI9kXWwE4riBTAIC3juPjYnJzAYC2/iwg2RjAyAIQgGd3tI05ACjHFmQ/M84l' + b'n4Qw90Fu3hsKs94oBINUBIN0hGY/CrrAELjAXkrvfaU9WDOS/bAVgGMBgBKXI54F+LJzWRAA' + b'cFBqOQiMAARbwtHaF3a1pwQAJxRlH50pS391QPqHcMbgy+5giK/6yUaZe7gzX/TeKABBGg2x' + b'1PHPtvc8P5L9sBWAo/MzasbFAWDEm5nNghADOIwYINliCDeB8PtbI7Hav7Z0pAQAR+RlyFmK' + b'ssEj0wlY08L2dQXDWLcTlUHj/42EgtF9QvoXbtX1wg+C4c6R7IetAHw7DwBwqgAAjQPQwwEQ' + b'myOSBQCXQdGFbItotX9r7UwJAFCOzMv8UY4q34IuDQcpGo2ynnCU1wSslLq/uv988HEV8O1X' + b'ukMHj3QfbAVgSa6/phgBAAVBAEO8GZkAQLsRBFoHoDkaq32lrTtlADgw2z++xCFvUmRJ5RU7' + b'CAbbgiEIBgkPBkdCvaIAxEgvYz95Kxj+6Uj3wVYAFuf4aooAAIeRBnr9GaynvYMrR0l+OwAH' + b'AH9/e1SrfbU9dQBAWZTjfyFPVZZi4IfrA7FIjIU1jTpJcqXufQn2O8z3AZCD3wyG3x7p9tsK' + b'wKHZAgA0kRjJe30AQBsCQIxScHLC00B43RHTal/v6EkpAJbkZvyHV6LPo1XTxClmrDMUoWj1' + b'5GFCwMTAYwDcoRFS8H/B8Ig/PtdWAA7J8tYUAgCKoRwPPkGrQ1gAmVhYCyBCETsBgDc7gykF' + b'wMHZPsUnSZsh8BuPiQ3C2heJ4O4NXvAabgaAwWUfI8+/2xs+3o722wrAQQiAQxEA6AiAl/V0' + b'dIo6QJKzgy+E8CIIAwD02re7elMKANFP308hJbwZBzzGj7PVWB+vCVBrzwseJHzjCVoBQmre' + b'7Y3ca0fbbQXgwExPTT4AoFJhAVweAKBTAKCQfd8ajsLvCyQiFWrR9Np3u/tSDoDZmd7yfFna' + b'qBiPBsVgsDscISp00codwYMFY58QRIHgAqatDkXX29F2WwGYnwEAqDK3ABjNOt0e1tvVxRdN' + b'5KTPCGTxIHAXAPB+TyjlAEA5INP7v5kyPQornGgFWFRjEU3nRa8hxQGY+YgYYOu7oUiZXe22' + b'FYB5Ge6afAVdgAgCTQB4EJhkKVhsiuR1cG4BVgfDKQnAwkzviS5K/ox95UfEgs8LRmIcgGSL' + b'XomiE1H86iXk4bpQ9Dy72m0rAHP97po8tADUsAAuN+vt7jLSQGt7AjEIbAUAPuxNTQDm+N2q' + b'W6KNEAwWY88w5omEY4wSXCZObul7QJ95AQgnCjnj3VD0CbvabSsAs32umtxEF+B0s77ubiMI' + b'tLAfgAgXgACs6Y2kJAAoczM8t7kpu0GhovJJgYJQTBOlbwuqZkbQG0HvR2nJmlB0p11tthWA' + b'WV5nTY6a4AIAgFB3DwBALK0FiNVAsAC6XlvXl7oATPd7Kv2ErYf+yegFKESw4AaIA3fxW3AD' + b'PP3jf4B8/EEoNsvONtsKwH4AQLYirzT9ogMAiPQEjUKQhVIw3xFESBsA8HEKA4Ayw+t6xSeR' + b'JRgMYuYiaRoDawCxQPJuQDfMf4eu/3JzTLf17CNbAZjhQQCklVj1w3ROdbpYtKeXuwArj9E1' + b's4A2jdV+EkptAAJe16kqZasUPD8ZT/sH8rkbQOhJEtRj/AgIhOHVLUlH/jMUfdnO9toKwHS3' + b'oyZLBQAo5bc1OZxOFg32CRdArNUB0AW06ax2bYoDMNXjdDopaVQJKUT18lNC8O4R+EFNMvCN' + b'8h1ABDca5q+L6raef2grAFPdak0mugAqXABaAK23T7gAYi0IxCygHQD4LMUBQKl2O34BKeF1' + b'PBjEh0fqOouiGyBJQI+1f9HnV9dGNNtPQLUVgCoXAiANBKAPLAAZggsApbSDC1gXjqY8AJPc' + b'jiovIetkkezwYDAc1Xk6qPQfo7pHEZtfAHjCbqiP6XfY3VZbAZjiUmoyZNlwARAEOpxMD4WM' + b'QlDyFkAzloTRAnyeBgCgTHY733AR/VDJSAllsAKQFfL456t6rYnVPzQE+38R0z+0u522AlDp' + b'RABEEJgIAC8FJ/3xRgzAeFRcuz4cSwsAqtyOM8D0P8atH/wsM52FIRRw4pkBX2EB+M4iQnbD' + b'/ygGAGx/DoKtAFQAAH4pAQDVyVgELAAZwloAvHaCBfgiTQCY6FRdKmFNAEAuP1Qb7+8HE4B5' + b'wN6qoGb+H2LsqUaNnTYa7bQVgIkOAECm/VmA6sD6KFcArwQmvRws0kAEoD6SHgCgjHOq9/gI' + b'uxL7j8UsBSjAmgBkCHu0AppY+sVBuQj8f+1otNFWAModsrAACTEACQsArO4JROV0Mlb7ZRoB' + b'UOpyzHDq2sfQfwlBlgHniCbWBvYUBAv/j2dEsooGjW0ejTbaCkCZKtf4MAagJG4BaCQSTwOT' + b'JcCMAboAgA1pBADXgVN9y8H0g3ADDFoxFayAhg8TG7wnkolUF/q6YZOmTx6t9tkKQCkA4JVo' + b'vwVAAKJRiptBZAubQvnZeEQAsDHNACh2KGe7GHvEnAQK9CiqE6Mm0K8DfjQs4QD8V6OmXzJa' + b'7bMVgPGKBAD0B4Gq4mBSNGLZBehGENilk9pN0fQCoESVPWDtmuDKpqI/jOIaAS4QkX4d8F1P' + b'hANy8gaN/Xm02mcrACUAgEdKdAHqAACsZAGa4QI2R7W0AgClwKn+xqVpl5vBoIqbhw03YJ6W' + b'iBYOXEAsQvTCHToZtaei2wrAOA5AvwtQAQCZuwBq/XwAtACM1DakIQBFDiWg6PpHMhYFCY9/' + b'eFEoHgsR4f/BUP5zk8YWjGbbbAWgGABwUyosAPzsUAwAjC1hyVsAUQzqBhfQEEs/AFCKVPk9' + b'SAPn863jqAsdD8InXBcoUbHieVszIz8czXbZCkCRQjkAmPNiMKcCAErMAMDCfQGauDee9IAF' + b'aLQOQLJH99kqeap8Pgz6g6Y7VEEjMcMNoER4mkgWbdHZP0azXbYCUCjTGhc1XADo3cEBiHEA' + b'kt0SZpaCteQA2JtZYYNeR10KFMknMR4MZprvSXiujNFe6GM3WLqCHYyELP5pfiqt8coGXUn9' + b'sn2dTgSAGQBosQTfZ2VPICFBndVu0fQ4AJBO0lh/5xMHP1EJerLKsFuyFem/ICW8SDKCQTxe' + b'RDPaDJ3461adHZvEnzEHnGfTxntawmWpr7YCkB8HwMgCFAUA0OJ1gH0+MZQYN4cSMYoAwIO9' + b'jF3SAwEUbpiN4RpL/8CbAx4j/b+S9EwYDclT5P1lpr+Pz1ZHi6hAF2OGEmCCXLOLkV/t5Vdx' + b'wHGwnXA5jJ+xjxHjig21n/YCINEaJ2YBRIyGCgA4AACZmJXAQR9vPiWUDJy+4sRsvkXmsSaN' + b'75FLNOlmCm1eGkmxgU+UPFlaLTM2F28Y0ngsQBj4fxolbGYHI2vN/wcjLOlisN1weYj4HgVd' + b'RK/xGjXUM2SxFYBcAMCVCICsMFU3LAD/dGo8GJrs4Yofkqz3EdIUZOydbp29CK7gS/jndrhw' + b'qxQelhwm/QM/Iocn2inZinKxomv3mToBXWAwuD1MWBm/ExgGGjriYSJWwAtnPc7wbrg64Ooy' + b'+pz6B0XmUAGAJMaZWwCVxwA0XgDpn+n9p2LjWzCyfWDqW2DQvwSFfEHEVQ9XA1y74fd79YEz' + b'Pi3EQ2mGh5Im+NZv1gQoY6taGKmB/uTBWwVwZRMx8D1wbYdrBxGDHyIjDLmtAGQCAKIQZMYA' + b'Mo8BzA2hfI2c9A96iJFwN2M9MPC7YVRb4L1tcG0gYvDxtQn+VDvMmlBUDHxail9VvgMvE8Hv' + b'e0AH2RTPl2IMrRrOeLRqCMiXxivO+CixCXLbAPBSflraFV6J/gpzfpzhwgJoiZ4fBz0Cgx6E' + b'AK8d7FwrvId3wWyFCw9DbDBe8b12AKeX4kO108DUDxZjQ7jKhHkvgu9xxQ+fBzieiMFthOsz' + b'uDbBhU9ART9vO+S2AKAK7477Hq7MluidhqsnTkXEAGEY9B4YdJjpnTCYbUaHcbZvMRSBAOCg' + b'o9kLwq+HqCiW6SyNzL0haPAwkMuBq5SIQZ9IhIlH4NGy4WzfBn3sxn7qo9hHWwAA2lXoRCb0' + b'/DgnJacTQXy2LEmOPl3vhaAHBxafdjV40NHXoSlE3xcfdJJCuXySwmc7XH4ifHo5XMVweYkw' + b'8c1EmPdd8B+74MIbgcbEqo00AGaBwm90Go81nUIE+ejfcBAxmsXAZotx4aC3Ge+bqU1iHp9O' + b'gv3HmZ1BRECXb+iCb2cw+tlmfG8s/4+tjBQAaOZw4DFX9RgKyCXC7PmIMIH4WejXcPa3GorA' + b'mY4zIkr6ixlp599J/8DjDM80XhWjX9hHhBvT1ghJMbCHCoD5e4kD7zCUYKb5ZnWOkIEFG1TC' + b'4Nw9ZQs3+xAceDT1LiIgN4s12De0ZibcYz7T9yZDAUAi/aVJhfQPOL5nzmCzHDu4OLOnmk86' + b'iunjHcarZPQT4TatWVpYsmQBSFxskRKuxMUXfdCVroO7Lz2YwMuk37INaSEmFSQZAAavtA3a' + b'y5r2szkZSQTf7LcJeloLHeK/mUr4d5DBwH+txNZS8DeS+vINAP/m8g0A/+by//Kr6Y9Iz7ne' + b'AAAAAElFTkSuQmCC') + + +SettingsIcon = PyEmbeddedImage( + b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAB0UlEQVR4Xo2RT0gbQRTG30Yb' + b'w5o2Em20pB4MtlgItkUo9VRQs0eDJ4UepEUQWkquPfQgqCUlAfHWSwOiDUQLLb0UZgUV2+hB' + b'UUhy6EWlf0gx0S60G9vGzXNmkhEsLtkHy/veN/v7eLMrQZVqebYYaGhqIGLW8pryY6JPFbPN' + b'Chy60wyxgWvAugirFsBhl/sSedTlgY5mJ2x80WBqLcs3ACvVHlnHyPIefvh8iFHa2cxCRXh7' + b'VMwm1Rb+hAvpHE4s7iDVZ2BfOInjKvVpN72CcWyA/s+Ao2KJ6hII2O6oI0M3PdDaKEPJMMD0' + b'L3jHlvCpch3YGzt5HeY3v3N/sMsLbY31sHugQ4J65wY4Q+8DzosyedLLA3h9PShw3eqWuX63' + b'9Y3ZinQe7HDYidLppWvWA6cAaZPKQblfQNJZ+HP0V/k93a9K/8MXamvIPb8XrrgZDJD9qcNK' + b'qrw+VuKKxwaHoTKfwnQg3f6rcNkl84N9rQDJDF9VACY1/DogP36L3dPrGJzLYHA2wzXz2JkA' + b'TGH76Bu8FU1iTyyFPa9SeJtq5nG4WtWOJPDGi4949+UWfba5pp41WGxgexBH3/NV9E2uou1h' + b'3DosPmIFIBVPgZn7qtWAE7Xe3IneUIDPAAAAAElFTkSuQmCC') diff --git a/Quick_IP_Converter.py b/Quick_IP_Converter.py new file mode 100644 index 0000000..bdf7c92 --- /dev/null +++ b/Quick_IP_Converter.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +# Copyright (C) 2014, 2018 Brandon M. Pace +# +# This file is part of Quick IP Converter +# +# Quick IP Converter is free software: you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Quick IP Converter 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with Quick IP Converter. +# If not, see . + + +import GUI +import libIPconv as conv +import wx + + +class MainFrame(GUI.IPConverterFrame): + def __init__(self, *args, **kwds): + GUI.IPConverterFrame.__init__(self, *args, **kwds) + self.last_changed = self.text_ctrl_hex + self.text_ctrl_dec.addr_type = conv.ADDRTYPE.DEC + self.text_ctrl_dotted.addr_type = conv.ADDRTYPE.DOTTED + self.text_ctrl_hex.addr_type = conv.ADDRTYPE.HEX + + main_converter.register_callback(self.text_ctrl_dec.ChangeValue, conv.ADDRTYPE.DEC) + main_converter.register_callback(self.text_ctrl_dotted.ChangeValue, conv.ADDRTYPE.DOTTED) + main_converter.register_callback(self.text_ctrl_hex.ChangeValue, conv.ADDRTYPE.HEX) + + main_converter.reverse = self.checkbox_reverse.IsChecked() + + def monitor_clipboard(self): + success, clipboard_content = super().monitor_clipboard() + if success and clipboard_content: + trimmed = clipboard_content.strip() + if conv.RECLIST[conv.ADDRTYPE.HEX].fullmatch(trimmed) and (trimmed != self.text_ctrl_hex.GetValue()): + self.text_ctrl_hex.SetValue(trimmed) + elif conv.DOTTEDQUADIP_STRICTREC.fullmatch(trimmed) and (trimmed != self.text_ctrl_dotted.GetValue()): + self.text_ctrl_dotted.SetValue(trimmed) + + def on_checkbox_reverse(self, event): + main_converter.reverse = event.IsChecked() + main_converter.set_value(self.last_selected.GetValue(), self.last_selected.addr_type) + + def on_char(self, event): + super().on_char(event) + + if event.GetSkipped(): + return # return if the key was already allowed + + event_control = event.GetEventObject() + event_key = event.GetKeyCode() + + if conv.filters.isAllowedASCII(event_key, event_control.addr_type): + event.Skip() + if event_control.addr_type == conv.ADDRTYPE.DOTTED: + control_content = event_control.GetValue() + control_selection = event_control.GetSelection() + + # Insert the new character at the insertion point, over-writing any selected characters + first = control_content[0:control_selection[0]] + last = control_content[control_selection[1]:] + check_value = first + chr(event_key) + last + + if event.GetSkipped(): + if '.' not in check_value: + check_value += '.' + if check_value.endswith('.'): + check_value += '0' + if not conv.isValidIPv4(check_value, conv.ADDRTYPE.DOTTED): + event.Skip(False) + else: + event.Skip() + + def on_paste(self, event): + success, pasted_string = super().on_paste(event) + + event_object = event.GetEventObject() + + filtered_string = conv.filters.filterChars(pasted_string, event_object.addr_type) + if not filtered_string: + return # nothing to insert + + control_content = event_object.GetValue() + control_selection = event_object.GetSelection() + + # Insert the new string at the insertion point, over-writing any selected characters + first = control_content[0:control_selection[0]] + last = control_content[control_selection[1]:] + final_value = first + filtered_string + last + + event_object.SetValue(final_value) + event_object.SetInsertionPoint(len(first + filtered_string)) + + def on_text(self, event): + event_object = event.GetEventObject() + main_converter.set_value(event_object.GetValue(), event_object.addr_type) + self.last_changed = event_object + + +class MainApp(wx.App): + def OnInit(self): + self.frame_main = MainFrame(None, wx.ID_ANY, "", name='MainFrame') + self.SetTopWindow(self.frame_main) + self.frame_main.Show() + return True + + +if __name__ == "__main__": + main_converter = conv.Converter() + app = MainApp(0) + app.MainLoop() diff --git a/README.md b/README.md index 01120bc..e9749c0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,92 @@ -# Quick-IP-Converter +# Quick IP Converter + +Copyright (C) 2014, 2018 Brandon M. Pace + +License: LGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this program. + If not, see . + Convert values between decimal, hexadecimal and dotted-quad IP formats + +Dotted Quad accepts IPv4 addresses or subnet masks in the following formats: + + - 192.168.10.1 + - 255.255.240.0 + +Hex accepts hex values with optional 0x or 0X prefix: + + - 0xc0a80101 + - 0X0A0A0A01 + - 1bb + +Decimal will accept any valid decimal value with no separators: + + - 123 + - 23456 + +**Notes:** + + - The clipboard monitoring action only runs when the application does not have focus. + - 2.0 release removed most hotkeys + - Reverse checkbox triggers conversion from the last selected text box + + +**Revision history:** + + 2.0: Completely new GUI that is smaller in size + - Removed IP checkbox + - Removed About button + - Added option to monitor clipboard for hex or dotted quad values once a second and automatically run conversion + - The clipboard is only monitored when another application has focus. + - Allow for much larger numbers for decimal-hex conversion + - All hotkeys removed except for Ctrl-Win-Z, which will take a hex value from the clipboard and run conversion + - Added a settings window, which contains the About link + - Added option for dark theme + - Added option to disable 'stay on top' + - Reverse option is now true byte-order flip for all conversions + - Reverse checkbox now triggers conversion from the last selected TextCtrl + - Paste now works properly instead of replacing the entire contents of the TextCtrl + - Window position and settings are saved when the exit button in the window is clicked + + 1.8: Updated to use Python 3.6.5 and wxPython Phoenix 4.0.3 + - Pressing Enter is no longer necessary to trigger calculation. You can type or paste values and calculation will automatically run. + - Clicking inside of a text box now selects all text if it wasn't already in focus. + - The text is not copied to the clipboard in case you are trying to paste current clipboard data in the same place. + - Each text box now only allows relevant characters to be placed there. + - Limitation: The largest decimal number supported is 18446744073709551615 (8 bytes, hex ffffffffffffffff) + + 1.6: Further enhancements to facilitate keyboard-only use. (even with the window not in focus, so these are Global) + - Added keyboard shortcuts that process data from the Windows clipboard and toggle the IP/Reverse checkboxes as needed: (Thanks goes to Dan Cross for the recommendation) + - Win + Z = Process Hex IP (enables IP) + - Win + X = Process Dotted-Quad IP (enables IP) + - Ctrl + Win + Z = Process Hex to Decimal + - Ctrl + Win + X = Process Decimal to Hex + - Win + A = Toggle Reverse + + If IP is unchecked and Reverse gets checked, both will get selected. + If IP and Reverse are both checked, and IP gets unchecked, both will get deselected. + + + 1.4: Focus was on user experience and allowing keyboard-only use: + - Removed buttons. Input is now processed upon pressing 'Enter' + - Added conversion to Decimal IP. + - Background color normalized + - Using the 'Tab' key to traverse the window is now enabled. + + 1.2: + - Added 'IP' checkbox to allow for non-IP conversion between decimal-hex. + + 1.0: + - Initial release with decimal-hex conversion and reverse option. diff --git a/libIPconv/__init__.py b/libIPconv/__init__.py new file mode 100644 index 0000000..46db00a --- /dev/null +++ b/libIPconv/__init__.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +# Copyright (C) 2014, 2018 Brandon M. Pace +# +# This file is part of Quick IP Converter +# +# Quick IP Converter is free software: you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Quick IP Converter 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with Quick IP Converter. +# If not, see . + + +from . import filters +from .converter import * diff --git a/libIPconv/conversions.py b/libIPconv/conversions.py new file mode 100644 index 0000000..b44b1c3 --- /dev/null +++ b/libIPconv/conversions.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +# Copyright (C) 2014, 2018 Brandon M. Pace +# +# This file is part of Quick IP Converter +# +# Quick IP Converter is free software: you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Quick IP Converter 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with Quick IP Converter. +# If not, see . + + +from .convregex import * +from .globals import * + + +def convertStrToType(input_val: str, input_type: int, output_type: int, reverse: bool = False, safe: bool = False) -> str: + """ + Be sure to use isValidIPv4() before this to sanity-check your input! + :param input_val: str value to convert + :param input_type: int value type from ADDRTYPE enum determining what the source type is + :param output_type: int value type from the ADDRTYPE enum determining what the destination type is + :param reverse: bool for whether or not to reverse the byte-order + :return: str converted value or '' on error/failure + """ + return_value = '' + try: + if input_type == output_type: + raise ValueError('output_type should be different than input_type') + + if input_type == ADDRTYPE.DEC: + if output_type == ADDRTYPE.DOTTED: + return_value = decStrToDottedQuadStr(input_val, reverse) + elif output_type == ADDRTYPE.HEX: + return_value = decStrToHexStr(input_val, reverse) + elif input_type == ADDRTYPE.DOTTED: + if output_type == ADDRTYPE.DEC: + return_value = dottedQuadStrToDecStr(input_val, reverse) + elif output_type == ADDRTYPE.HEX: + return_value = dottedQuadStrToHexStr(input_val, reverse) + elif input_type == ADDRTYPE.HEX: + if output_type == ADDRTYPE.DEC: + return_value = hexStrToDecStr(input_val, reverse) + elif output_type == ADDRTYPE.DOTTED: + int_value = hexStrToDecStr(input_val, False) + return_value = decStrToDottedQuadStr(int_value, reverse) + except ValueError: + if not safe: + raise + else: + return '' + else: + return return_value + + +def decStrToDottedQuadStr(input_value: str, reverse: bool = False) -> str: + if RECLIST[ADDRTYPE.DEC].fullmatch(input_value): + return decToDottedQuadStr(int(input_value), reverse) + else: + return '' + + +def decToDottedQuadList(input_value: int, reverse: bool = False) -> list: + if (input_value < 0) or (input_value > V4MAXVAL): + raise ValueError(f'Input value is outside of IPv4 range: {input_value}') + if reverse: + byte_order = 'little' + else: + byte_order = 'big' + return [octet for octet in input_value.to_bytes(4, byte_order)] + + +def decStrToHexStr(input_value: str, reverse: bool = False) -> str: + return_value = '' + if input_value: + if reverse: + byte_order = 'little' + else: + byte_order = 'big' + int_value = int(input_value) + return_value = int_value.to_bytes(((int_value.bit_length() + 7) // 8), byte_order).hex() + return return_value + + +def decToDottedQuadStr(input_value: int, reverse: bool = False) -> str: + return '.'.join([str(octet) for octet in decToDottedQuadList(input_value, reverse)]) + + +def dottedQuadStrToDecStr(input_value: str, reverse: bool = False) -> str: + split_value = [int(value) for value in input_value.split('.')] + if reverse: + byte_order = 'little' + else: + byte_order = 'big' + return str(int.from_bytes(split_value, byte_order)) + + +def dottedQuadStrToHexStr(input_value: str, reverse: bool = False) -> str: + split_value = [int(value) for value in input_value.split('.')] + if reverse: + split_value.reverse() + byte_value = bytes(split_value) + return byte_value.hex() + + +def hexStrToDec(input_value: str, reverse: bool = False) -> int: + if RECLIST[ADDRTYPE.HEX].fullmatch(input_value): + trimmed = input_value.lstrip('0xX') + if len(trimmed) % 2: + trimmed = '0' + trimmed + byte_value = bytes.fromhex(trimmed) + if reverse: + byte_order = 'little' + else: + byte_order = 'big' + return int.from_bytes(byte_value, byte_order) + else: + return -1 + + +def hexStrToDecStr(hex_addr: str, reverse: bool = False) -> str: + check_value = hexStrToDec(hex_addr, reverse) + return '' if (check_value == -1) else str(check_value) + + +def isValidIPv4(input_value, addr_type: int = ADDRTYPE.NONE, strict: bool = False) -> bool: + """ + Accepts string or int value and returns True if it is a value in the range of valid IPv4 addresses. + The addr_type argument should be a value from the ADDRTYPE enum. If input is int you must use NONE or DEC addr_type. + The string can be a dotted-quad format, hex format, or decimal format. (no leading or trailing whitespace) + strict mode will require dotted-quad format to include four valid octets + """ + check_value = -1 + + if isinstance(input_value, str): + if '.' in input_value and (addr_type in [ADDRTYPE.NONE, ADDRTYPE.DOTTED]): + # Confirm match for dotted-quad format + if len(input_value) <= 15 and IP_RECLIST[ADDRTYPE.DOTTED].fullmatch(input_value): + if strict and not DOTTEDQUADIP_STRICTREC.fullmatch(input_value): + check_value = -2 + else: + check_value = 0 + else: + check_value = -3 + elif IP_RECLIST[ADDRTYPE.DEC].fullmatch(input_value) and (addr_type in [ADDRTYPE.NONE, ADDRTYPE.DEC]): + check_value = int(input_value) + elif IP_RECLIST[ADDRTYPE.HEX].fullmatch(input_value) and (addr_type in [ADDRTYPE.NONE, ADDRTYPE.HEX]): + check_value = int(input_value, 16) + elif isinstance(input_value, int): + if addr_type in [ADDRTYPE.NONE, ADDRTYPE.DEC]: + check_value = input_value + else: + raise ValueError('Type int input_value passed with incompatible addr_type of {addr_type}') + else: + raise ValueError(f'Expected input type str or int. Got {type(input_value)}') + + return (check_value >= 0) and (check_value <= V4MAXVAL) diff --git a/libIPconv/converter.py b/libIPconv/converter.py new file mode 100644 index 0000000..0c6d436 --- /dev/null +++ b/libIPconv/converter.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +# Copyright (C) 2014, 2018 Brandon M. Pace +# +# This file is part of Quick IP Converter +# +# Quick IP Converter is free software: you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Quick IP Converter 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with Quick IP Converter. +# If not, see . + + +from .conversions import * + + +class Converter(object): + _supported_addr_types = [ADDRTYPE.DEC, ADDRTYPE.DOTTED, ADDRTYPE.HEX] + + def __init__(self, safe=True): + self.reverse = False + self.safe = safe + self._callbacks = {key: None for key in self._supported_addr_types} + self._values = {key: '' for key in self._supported_addr_types} + + def is_addr_type_supported(self, addr_type: int) -> bool: + return addr_type in self._supported_addr_types + + def register_callback(self, callback_function, addr_type: int): + """ + Allows calling a single-argument function for values changed as a result of set_value. + When one value changes, callbacks for the others are called with the converted string value, + which can be '' if the set value is invalid. + :param callback_function: function that takes a single argument (the new value) + :param addr_type: int from the ADDRTYPE enum + :return: None + """ + if not callable(callback_function): + raise ValueError('callback_function is not callable') + self._check_addr_type(addr_type) + + self._callbacks[addr_type] = callback_function + + def reset_values(self): + for key in self._values.keys(): + self._values[key] = '' + + def run_callbacks(self, addr_type: int): + """Run callback functions for types other than the input addr_type""" + for typeval in self._supported_addr_types: + if (typeval != addr_type) and self._callbacks[typeval]: + self._callbacks[typeval](self._values[typeval]) + + def run_conversions(self, addr_type: int): + """Convert the currently stored value of addr_type to other types""" + self._check_addr_type(addr_type) + for typeval in self._supported_addr_types: + if typeval != addr_type: + self._values[typeval] = convertStrToType( + self._values[addr_type], addr_type, typeval, reverse=self.reverse, safe=self.safe) + + def set_value(self, value: str, addr_type: int): + self._check_addr_type(addr_type) + + self._values[addr_type] = value + + if not value: + self.reset_values() + else: + self.run_conversions(addr_type) + + # call callbacks + self.run_callbacks(addr_type) + + def _check_addr_type(self, addr_type: int): + if not self.is_addr_type_supported(addr_type): + raise ValueError(f'addr_type value of {addr_type} is not supported') diff --git a/libIPconv/convregex.py b/libIPconv/convregex.py new file mode 100644 index 0000000..f8d1aa1 --- /dev/null +++ b/libIPconv/convregex.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +# Copyright (C) 2014, 2018 Brandon M. Pace +# +# This file is part of Quick IP Converter +# +# Quick IP Converter is free software: you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Quick IP Converter 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with Quick IP Converter. +# If not, see . + + +import re + + +# regex for matching decimal, dotted-quad and hex IP - including compiled versions for performance +DECIP_RE = r'^([0-9]{1,10})$' +DECIP_REC = re.compile(DECIP_RE) + +HEXIP_RE = r'^((0[xX])?[0-9a-fA-F]{1,8})$' +HEXIP_REC = re.compile(HEXIP_RE) + +# Requires 0-255 before and after each '.', up to 3 instances of '.' +DOTTEDQUADIP_RE = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){0,3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' +DOTTEDQUADIP_REC = re.compile(DOTTEDQUADIP_RE) + +IP_RELIST = [DECIP_RE, HEXIP_RE, DOTTEDQUADIP_RE] +IP_RECLIST = [DECIP_REC, HEXIP_REC, DOTTEDQUADIP_REC] + +# Requires 0-255 before and after each '.' with 3 instances of '.' +DOTTEDQUADIP_STRICTRE = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' +DOTTEDQUADIP_STRICTREC = re.compile(DOTTEDQUADIP_STRICTRE) + +# regex for matching general decimal and hex - including compiled versions for performance +DEC_RE = r'^([0-9]+)$' +DEC_REC = re.compile(DEC_RE) + +HEX_RE = r'^((0[xX])?[0-9a-fA-F]+)$' +HEX_REC = re.compile(HEX_RE) + +RELIST = [DEC_RE, HEX_RE] +RECLIST = [DEC_REC, HEX_REC] diff --git a/libIPconv/filters.py b/libIPconv/filters.py new file mode 100644 index 0000000..17b907d --- /dev/null +++ b/libIPconv/filters.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +# Copyright (C) 2014, 2018 Brandon M. Pace +# +# This file is part of Quick IP Converter +# +# Quick IP Converter is free software: you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Quick IP Converter 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with Quick IP Converter. +# If not, see . + + +import string +from .globals import ADDRTYPE + + +dec_allowed_ascii = [ord(ch) for ch in string.digits] +dotted_allowed_ascii = dec_allowed_ascii + [ord('.')] +hex_allowed_ascii = [ord(ch) for ch in string.hexdigits] + + +def filterASCII(key_codes: list, addr_type: int) -> list: + if addr_type == ADDRTYPE.DEC: + retval = [key for key in key_codes if key in dec_allowed_ascii] + elif addr_type == ADDRTYPE.DOTTED: + retval = [key for key in key_codes if key in dotted_allowed_ascii] + elif addr_type == ADDRTYPE.HEX: + retval = [key for key in key_codes if key in hex_allowed_ascii] + else: + raise ValueError(f'addr_type of {addr_type} is not valid') + return retval + + +def filterChars(chars: str, addr_type: int) -> str: + if addr_type == ADDRTYPE.DEC: + retval = ''.join(ch for ch in chars if ch in string.digits) + elif addr_type == ADDRTYPE.DOTTED: + retval = ''.join(ch for ch in chars if ch in string.digits or ch in '.') + elif addr_type == ADDRTYPE.HEX: + retval = ''.join(ch for ch in chars if ch in string.hexdigits) + else: + raise ValueError(f'addr_type of {addr_type} is not valid') + return retval + + +def isAllowedASCII(key_code: int, addr_type: int) -> bool: + if addr_type == ADDRTYPE.DEC: + retval = key_code in dec_allowed_ascii + elif addr_type == ADDRTYPE.DOTTED: + retval = key_code in dotted_allowed_ascii + elif addr_type == ADDRTYPE.HEX: + retval = key_code in hex_allowed_ascii + else: + raise ValueError(f'addr_type of {addr_type} is not valid') + return retval + + +def isAllowedChar(char: str, addr_type: int) -> bool: + if (len(char) != 1): + raise ValueError(f'char input includes more than one character: {char}') + if addr_type == ADDRTYPE.DEC: + retval = char in string.digits + elif addr_type == ADDRTYPE.DOTTED: + retval = char in string.digits or char in '.' + elif addr_type == ADDRTYPE.HEX: + retval = char in string.hexdigits + else: + raise ValueError(f'addr_type of {addr_type} is not valid') + return retval diff --git a/libIPconv/globals.py b/libIPconv/globals.py new file mode 100644 index 0000000..0923322 --- /dev/null +++ b/libIPconv/globals.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +# Copyright (C) 2014, 2018 Brandon M. Pace +# +# This file is part of Quick IP Converter +# +# Quick IP Converter is free software: you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Quick IP Converter 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with Quick IP Converter. +# If not, see . + + +import enum + + +# list of bit-masks for each octet of an IPv4 address +V4MASKS = [255 << (8*octet_offset) for octet_offset in range(4)] + + +V4MAXVAL = int('0xffffffff', 16) # 4294967295 or 255.255.255.255 + + +# enum values that can be used for index of the IP_RE*LIST items +@enum.unique +class ADDRTYPE(enum.IntEnum): + NONE = -1 + DEC = 0 + HEX = 1 + DOTTED = 2 diff --git a/pyinstaller_build.bat b/pyinstaller_build.bat new file mode 100644 index 0000000..50a103d --- /dev/null +++ b/pyinstaller_build.bat @@ -0,0 +1 @@ +python.exe -OO -m PyInstaller -w --version-file pyinstaller_version_file.txt --icon=GUI\raw_resources\IPconv.ico Quick_IP_Converter.py diff --git a/pyinstaller_version_file.txt b/pyinstaller_version_file.txt new file mode 100644 index 0000000..3235f95 --- /dev/null +++ b/pyinstaller_version_file.txt @@ -0,0 +1,55 @@ +# UTF-8 +# +# For more details about fixed file info 'ffi' see: +# http://msdn.microsoft.com/en-us/library/ms646997.aspx +# HOW TO UPDATE: +# Change the filevers variable +# Change the ProductVersion in the StringTable +# To use this file when running PyInstaller: python.exe -OO -m PyInstaller -w --version-file pyinstaller_version_file.txt --icon=GUI\raw_resources\IPconv.ico Quick_IP_Converter.py +# (-OO is optional and is for additional bytecode optimization) +VSVersionInfo( + ffi=FixedFileInfo( + # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) + # Set not needed items to zero 0. + # NOTE: change filevers ONLY. To change product version, edit ProductVersion in the StringTable! + # LEAVE the 4-part versioning, just use 0 where you don't have any version info. Ex: 4.4 will be 4, 4, 0, 0 + filevers=(2, 0, 0, 0), + prodvers=(0, 0, 0, 0), + # Contains a bitmask that specifies the valid bits 'flags'r + mask=0x3f, + # Contains a bitmask that specifies the Boolean attributes of the file. + flags=0x0, + # The operating system for which this file was designed. + # 0x4 - NT and there is no need to change it. + OS=0x4, + # The general type of file. + # 0x1 - the file is an application. + fileType=0x1, + # The function of the file. + # 0x0 - the function is not defined for this fileType + subtype=0x0, + # Creation date and time stamp. + date=(0, 0) + ), + kids=[ + VarFileInfo([VarStruct(u'Translation', [0, 1200])]), + StringFileInfo( + [ + StringTable( + u'000004b0', + [StringStruct(u'Comments', u''), + StringStruct(u'CompanyName', u''), + StringStruct(u'FileDescription', u'IPv4 value conversion program'), + # NOTE: This 'FileVersion' is over-written with the filevers contents! Change that instead! + StringStruct(u'FileVersion', u'0.0.0.0'), + StringStruct(u'InternalName', u''), + StringStruct(u'LegalCopyright', u'Copyright © Brandon M. Pace 2014, 2018'), + StringStruct(u'OriginalFilename', u'Quick_IP_Converter.exe'), + StringStruct(u'ProductName', u'Quick IP Converter'), + # NOTE: This is where you should change the version: + StringStruct(u'ProductVersion', u'2.0.0.0'), + # Don't worry about changing the below at all + StringStruct(u'Assembly Version', u'0.0.0.0')]) + ]) + ] +)