From 52ae16df3bef978d8e69efee1cc38a367ad418fc Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 16 Jan 2021 14:20:03 +0100 Subject: [PATCH] Initial commit --- fillscreen.py | 37 +++++ fillscreen.ui | 25 ++++ grail.py | 386 ++++++++++++++++++++++++++++++++++++++++++++++++ grail_build.cfg | 11 ++ selector.py | 105 +++++++++++++ 5 files changed, 564 insertions(+) create mode 100644 fillscreen.py create mode 100644 fillscreen.ui create mode 100644 grail.py create mode 100644 grail_build.cfg create mode 100644 selector.py diff --git a/fillscreen.py b/fillscreen.py new file mode 100644 index 0000000..5225969 --- /dev/null +++ b/fillscreen.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'C:\Users\lmahler\Desktop\Dump\02_Python\Grail\fillscreen.ui' +# +# Created by: PyQt4 UI code generator 4.11.4 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) + +class Ui_View(object): + def setupUi(self, View): + View.setObjectName(_fromUtf8("View")) + View.resize(294, 166) + View.setWindowOpacity(0.3) + View.setStyleSheet(_fromUtf8("")) + + self.retranslateUi(View) + QtCore.QMetaObject.connectSlotsByName(View) + + def retranslateUi(self, View): + View.setWindowTitle(_translate("View", "Grail.exe", None)) + diff --git a/fillscreen.ui b/fillscreen.ui new file mode 100644 index 0000000..d3fa67e --- /dev/null +++ b/fillscreen.ui @@ -0,0 +1,25 @@ + + + View + + + + 0 + 0 + 294 + 166 + + + + Grail.exe + + + 0.300000000000000 + + + + + + + + diff --git a/grail.py b/grail.py new file mode 100644 index 0000000..23cef75 --- /dev/null +++ b/grail.py @@ -0,0 +1,386 @@ +""" +#*********************************# +#* Grail *# +#*********************************# +#* Author: Lukas Mahler *# +#* Copyright: (C)2018-2020 *# +#* Version: 0.5.0 *# +#* Date: 25.12.2020 *# +#*********************************# +# Instant Upload Screenshot-Tool # +# based on the idea of Gyazoo. # +# -> MS Windows only! # +#*********************************# +""" + +__author__ = "Lukas Mahler" +__copyright__ = "Copyright 2018-2021" +__version__ = "0.5.1" +__date__ = "16.01.2021" +__email__ = "lm@ankerlab.de" +__status__ = "Development" + + +try: + # Defaults + import os + import sys + import webbrowser + from io import BytesIO + from shutil import rmtree + from getpass import getpass + from tempfile import gettempdir + from socket import create_connection + from configparser import ConfigParser + from time import gmtime, strftime, sleep + + # GUI + from PyQt4 import QtGui, QtCore, Qt + from PyQt4.QtCore import QProcess, QPoint + from fillscreen import Ui_View + + # Customs + import win32gui + import win32clipboard + import paramiko + from desktopmagic.screengrab_win32 import getRectAsImage + +except ImportError as e: + print("ERROR: Missing Module |||", e) + sleep(10) + os.system("PAUSE>NUL") + sys.exit() + + +################################################################################################### +################################################################################################### + + +class Fillscreen(QtGui.QWidget, Ui_View): + + def __init__(self, parent=None): + QtGui.QWidget.__init__(self, parent) + + ############################################################################################### + ############################################################################################### + + # Bastelstüble wegen Transparentem Window welches aber den Input auf unterliegendes blockieren soll + + # self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + # self.setBackgroundRole(QtGui.QPalette.Base) + # self.setAttribute(Qt.Qt.WA_NoSystemBackground, True) + self.setWindowFlags( + QtCore.Qt.WindowStaysOnTopHint | + QtCore.Qt.FramelessWindowHint | + QtCore.Qt.X11BypassWindowManagerHint + ) + # self.istransparent = True + + self.setupUi(self) + + # self.setWindowOpacity(0.3) + # Qcolor = "rgba(255, 255, 255, 0)" + # self.setStyleSheet("background-color:{0}".format(Qcolor)) + + ############################################################################################### + ############################################################################################### + + # Change Cursor to select tool on Startup + self.change_cursor() + self.begin = QtCore.QPoint() + self.end = QtCore.QPoint() + self.startingpoint = QtCore.QPoint() + self.endpoint = QtCore.QPoint() + + def showFullscreen(self): + self.showMaximized() + + # https://stackoverflow.com/a/44468898/5593051 + def paintEvent(self, event): + qp = QtGui.QPainter(self) + # br = QtGui.QBrush(QtGui.QColor(100, 10, 10, 40)) + qp.setPen(QtGui.QPen(QtCore.Qt.red, 3)) + qp.drawRect(QtCore.QRect(self.begin, self.end)) + + def mousePressEvent(self, event): + if event.button() == QtCore.Qt.RightButton: + print("DEBUG: Exiting on Right Click Request") + app.quit() + else: + self.begin = event.pos() + self.end = event.pos() + # self.startingpoint = event.pos() + self.startingpoint = get_real_pos() + # print("Startpoint:",self.startingpoint) + self.update() + + def mouseMoveEvent(self, event): + self.end = event.pos() + self.update() + + def mouseReleaseEvent(self, event): + self.begin = event.pos() + self.end = event.pos() + self.endpoint = event.pos() + self.endpoint = get_real_pos() + # print("Endpoint:",self.endpoint) + self.update() + do_screen(self.startingpoint, self.endpoint) + app.quit() + + @staticmethod + def change_cursor(): + # http://doc.qt.io/archives/qt-4.8/qcursor.html + QtGui.QApplication.setOverrideCursor(QtCore.Qt.CrossCursor) + # QtGui.QApplication.restoreOverrideCursor() + + +################################################################################################### +################################################################################################### + + +def read_ini(rootdir): + myini = {} + config = ConfigParser() + try: + config.read_file(open(rootdir+'settings.ini')) + except Exception as e: + print("ERROR: Couldn't locate a 'settings.ini' |||", e) + # Creating new blank ini + with open(rootdir+'settings.ini', 'w') as f: + txt = "[MAIN]\n" \ + "# MODE EITHER ONLINE OR OFFLINE\nMODE = OFFLINE\n\n\n" \ + "# IF MODE IS ONLINE PROVIDE THE SFTP INFO BELOW\n" \ + "[ONLINE]\n\n" \ + "# URL EITHER IS AN IP OR A FQDN\nURL = \n\n" \ + "# PORT IS THE SSH PORT\nPORT = 22\n\n" \ + "# THE TIME IN SECONDS TO TRY TO CONNECT\nTIMEOUT = 5\n\n" \ + "# YOUR SSH USER, THIS CAN BE BLANK TO GET ASKED INTERACTIVELY\nUSR = \nPASS = \n\n" \ + "# THE LOCAL SERVER PATH WHERE TO PUT THE SCREENSHOT\nPTH = ./Screens/\n\n" \ + "# THIS SHOULD BE THE FULL ONLINE URL\nWHERE = \n\n\n" \ + "# IF MODE IS OFFLINE YOU MAY CHANGE THE SETTINGS BELOW\n" \ + "[OFFLINE]\n\n" \ + "WHERE = " + f.write(txt) + + mode = config.get('MAIN', 'MODE') + if mode == "ONLINE": + url = config.get('ONLINE', 'URL') + port = int(config.get('ONLINE', 'PORT')) + timeout = int(config.get('ONLINE', 'TIMEOUT')) + usr = config.get('ONLINE', 'USR') + passw = config.get('ONLINE', 'PASS') + pth = config.get('ONLINE', 'PTH') + where = config.get('ONLINE', 'WHERE') + myini.update( + mode = mode, + URL = url, + PORT = port, + TIMEOUT = timeout, + USR = usr, + PASS = passw, + PTH = pth, + WHERE = where + ) + elif mode == "OFFLINE": + where = config.get('OFFLINE', 'WHERE') + myini.update( + mode = mode, + WHERE = where + ) + else: + print("ERROR: you seem to be having a corrupt or broken 'settings.ini' please recheck!") + + return myini + + +def get_real_pos(): + flags, hcursor, (x, y) = win32gui.GetCursorInfo() + return {"x": x, "y": y} + + +def calc_frame(startinspot, endspot): + # If dragging began from top + if startinspot['x'] < endspot['x']: + left = startinspot['x'] + right = endspot['x'] + top = startinspot['y'] + bottom = endspot['y'] + # If dragging began from bottom + else: + left = endspot['x'] + right = startinspot['x'] + top = endspot['y'] + bottom = startinspot['y'] + + # print("DEBUG: Start:",left,"x",top) + # print("DEBUG: End :",right,"x",bottom) + + if abs(left-right) <= 10 and abs(top-bottom) <= 10: + coordinates = None + else: + coordinates = (left, top, right+1, bottom+1) + + return coordinates + + +def do_screen(start, end): + """ This Function somehow is the Brain now """ + # QtGui.QPixmap.grabWindow(QtGui.QApplication.desktop().winId()).save('screenshot.jpg', 'jpg') + # Hide all windows to take a Screenshot of the underlying interface + for window in windows: + window.hide() + frame = calc_frame(start, end) + print("Took Snapshot on Coordinates:", frame) + # https://github.com/python-pillow/Pillow/issues/1547 + # https://github.com/ludios/Desktopmagic/blob/master/desktopmagic/screengrab_win32.py + # im = ImageGrab.grab(bbox=frame) + + if not frame: + print("ERROR: Screenshots below 5x5 px not allowed, exiting...") + sleep(5) + return + + im = getRectAsImage(frame) + send_to_clipboard(im) + timestamp = strftime("%Y-%m-%d_%H.%M.%S") + + tempdir = gettempdir()+"\\Screens" + # print("DEBUG:",tempdir) + if not os.path.exists(tempdir): + os.makedirs(tempdir) + tempsave = '{0}\\{1}.png'.format(tempdir, 'Screen_'+timestamp) + im.save(tempsave) + + if settings['MODE'] == "ONLINE": + upload_to_url(tempsave) + else: + save_to_local(tempsave) + + clean_temp(tempdir) + + +# https://stackoverflow.com/questions/34322132/copy-image-to-clipboard-in-python3 +def send_to_clipboard(im=None): + if im: + output = BytesIO() + im.convert("RGB").save(output, "BMP") + data = output.getvalue()[14:] + output.close() + win32clipboard.OpenClipboard() + win32clipboard.EmptyClipboard() + win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data) + win32clipboard.CloseClipboard() + print("Copied to Clipboard") + else: + win32clipboard.EmptyClipboard() + print("Cleared Clipboard") + + +# https://stackoverflow.com/questions/432385/sftp-in-python-platform-independent +def upload_to_url(tempsave): + + # SFTP INFO + url = settings['URL'] + port = settings['PORT'] + timeout = settings['TIMEOUT'] + usr = settings['USR'] + passw = settings['PASS'] + pth = settings['PTH'] + os.path.basename(tempsave) + where = settings['WHERE'] + os.path.basename(tempsave) + + # Test Connection first or go into fallback + try: + s = create_connection((url, port), timeout) + s.close() + except Exception as e: + print("ERROR: Connection failed. |||", e) + print("\nFalling back to local save...") + save_to_local(tempsave, fallback=1) + return + + # If not provided ask for Username & Password + if not usr: + usr = input("Please provide your Username: ") + + if not passw: + passw = getpass("Please provide the password for {0}: ".format(usr)) + + try: + transport = paramiko.Transport((url, port)) + transport.connect(username=usr, password=passw) + except Exception as e: + print("ERROR: SSH2 negotiation or authentication failed. |||", e) + print("\nFalling back to local save...") + save_to_local(tempsave, fallback=1) + return + + print("Starting to Upload...") + sftp = paramiko.SFTPClient.from_transport(transport) + sftp.put(tempsave, pth) + sftp.close() + transport.close() + + print("Screen was Uploaded: URL: " + where) + open_url(where) + + +def save_to_local(tempsave, fallback=None): + + if not fallback: + where = settings['WHERE'] + else: + print(" -> Did fallback!") + config = ConfigParser() + config.read_file(open(root+'_settings.ini')) + where = config.get('OFFLINE', 'WHERE') + + if not where: + print("No local Screens folder provided, creating one...") + where = os.path.dirname(os.path.abspath(__file__))+"\\Screens" + + print("OFFLINE MODE: Screenshot Folder:", where) + + if not os.path.exists(where): + os.makedirs(where) + + src = tempsave + dst = where + "\\" + os.path.basename(tempsave) + # print("Source:",src,"\nDestination:",dst) + os.rename(src, dst) + + +def open_url(where): + webbrowser.open(where, new=0, autoraise=True) + + +def clean_temp(tempdir): + if os.path.exists(tempdir): + print("Cleaning up the tempdir [", tempdir, "]...") + rmtree(tempdir, ignore_errors=True) + + +################################################################################################### +################################################################################################### + + +if __name__ == '__main__': + + root = os.path.dirname(os.path.abspath(__file__))+"\\" + settings = read_ini(root) + if os.path.exists(root+"qt.conf"): + try: + os.remove(root+"qt.conf") + except Exception as e: + print("DEBUG:", e) + app = QtGui.QApplication(sys.argv) + windows = [] + + # https://stackoverflow.com/questions/51058236/create-qt-windows-based-on-number-of-list-items + for i in range(QtGui.QApplication.desktop().screenCount()): + topLeft = QtGui.QApplication.desktop().screenGeometry(i).topLeft() + window = Fillscreen() + window.move(topLeft) + window.showFullscreen() + windows.append(window) + app.exec_() diff --git a/grail_build.cfg b/grail_build.cfg new file mode 100644 index 0000000..8c481a0 --- /dev/null +++ b/grail_build.cfg @@ -0,0 +1,11 @@ +[ship] +param = --onefile --windowed +icon = C:\Users\lukas\Desktop\Dump\08_Resources\camera.ico + +[debug] +param = --onefile +icon = C:\Users\lukas\Desktop\Dump\08_Resources\camera.ico + +[test] +param = --onedir --noupx --add-binary msvcp140.dll;. +icon = C:\Users\lukas\Desktop\Dump\08_Resources\camera.ico \ No newline at end of file diff --git a/selector.py b/selector.py new file mode 100644 index 0000000..f6dfbcc --- /dev/null +++ b/selector.py @@ -0,0 +1,105 @@ +""" +This subfile to Grail handles window settings and calculations. +-> Only works on MS Windows! +""" + +__author__ = "Lukas Mahler" +__copyright__ = "Copyright 2018-2020" +__version__ = "1.0.0" +__date__ = "25.11.2020" +__email__ = "lm@ankerlab.de" +__status__ = "Development" + + +import win32api +import win32gui + + +def getCursorPos(): + flags, hcursor, (x, y) = win32gui.GetCursorInfo() + return {"x": x, "y": y} + + +def getMonitors(): + monitors = [] + for hMonitor, hdcMonitor, pyRect in win32api.EnumDisplayMonitors(): + monitors.append(pyRect) + #print(monitors) + return monitors + + +def getResolution(monitors): + resolutions = {} + index = 0 + for monitor in monitors: + x_from = monitor[0] + x_to = monitor[2] + y_from = monitor[1] + y_to = monitor[3] + res_x = x_to - x_from + res_y = y_to - y_from + res = (res_x, res_y) + resolutions['Monitor'+str(index)] = res + index += 1 + + return resolutions + + +def getScreenInfo(): + return {"x": win32api.GetSystemMetrics(0), "y": win32api.GetSystemMetrics(1)} + + +def getStartingpoint(): + return getCursorPos() + + +def getEndpoint(): + return getCursorPos() + + +def getRectangle(): + + # Source: https://stackoverflow.com/a/41930485/5593051 + state_left = win32api.GetKeyState(0x01) # Left button down = 0 or 1. Button up = -127 or -128 + state_right = win32api.GetKeyState(0x02) # Right button down = 0 or 1. Button up = -127 or -128 + + while True: + a = win32api.GetKeyState(0x01) + b = win32api.GetKeyState(0x02) + + if a != state_left: # Button state changed + state_left = a + if a < 0: + print('Left Button Pressed') + pos_start = getStartingpoint() + else: + print('Left Button Released') + pos_end = getEndpoint() + break + + print(pos_start) + print(pos_end) + + calcScreenshotFrame(pos_start, pos_end) + + +def calcScreenshotFrame(pos_start, pos_end): + # If dragging began from top + if pos_start['x'] < pos_end['x']: + left = pos_start['x'] + right = pos_end['x'] + top = pos_start['y'] + bottom = pos_end['y'] + # If dragging began from bottom + else: + left = pos_end['x'] + right = pos_start['x'] + top = pos_end['y'] + bottom = pos_start['y'] + + print("Screenshot needs to be taken from: x:", left, "->", right, "and y:", top, "->", bottom) + + +if __name__ == '__main__': + print("This is a Subfile from grail, it can't be executed on its own.") + exit()