From 0d4ffb1f05e57c73f0ad3874926c1defcea7233c Mon Sep 17 00:00:00 2001 From: Binit Date: Mon, 3 Jul 2023 18:36:01 +0530 Subject: [PATCH] Refactored & updated documentation --- README.md | 6 +- __pycache__/utils.cpython-39.pyc | Bin 0 -> 4213 bytes clipette.py | 550 ------------------- clipette/__init__.py | 417 ++++++++++++++ clipette/__pycache__/__init__.cpython-39.pyc | Bin 0 -> 11137 bytes clipette/__pycache__/utils.cpython-39.pyc | Bin 0 -> 4222 bytes clipette/utils.py | 183 ++++++ example.py | 5 + 8 files changed, 608 insertions(+), 553 deletions(-) create mode 100644 __pycache__/utils.cpython-39.pyc delete mode 100644 clipette.py create mode 100644 clipette/__init__.py create mode 100644 clipette/__pycache__/__init__.cpython-39.pyc create mode 100644 clipette/__pycache__/utils.cpython-39.pyc create mode 100644 clipette/utils.py create mode 100644 example.py diff --git a/README.md b/README.md index e971fcb..80af107 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ Python clipboard utility that works natively on python with its inbuilt modules **-WORK IN PROGRESS-** ## Usage -Must call `open_clipboard()` before using any clipboard function. Preferably through an `if` statement. +Must call `open_clipboard()` before using any clipboard function which returns 0 on failure. Should call `empty_clipboard()` before setting any data to clipboard. -Must call `close_clipboard()` at the end. +Must call `close_clipboard()` at the end or other applications may not be able to access the clipboard. Example (to get unicode text from clipboard): ``` -from . import clipette +import clipette if clipette.open_clipboard(): text = clipette.get_UNICODETEXT() diff --git a/__pycache__/utils.cpython-39.pyc b/__pycache__/utils.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5cf32c354f9f7d78d6e0948bfcdf28193657d34c GIT binary patch literal 4213 zcmdT{OLN=E5#|$oh@>8tELpZ5mL=Kp?%J^}WOu#JmPiqzn59UDq9tp}RVa`%lp%qS z0H9OOHTfBPNOIdt@(1!Wa+*`Bl8a6`<&spY(%k?llJag2$t7gro1U*{8kp|+z@d>& zCj;>L*B^eze}6j=_zQMMKXcI8gUiW<0s$W2K`X$p4Ozi@Km;yA7hx8rHo_v*Mp=}H znIZzei9tX9+aOEu2ut!Pn*u(?6_(~Pmf-<5&EsqaxLKZHb3Dmz@hLXXQ*42!d4^B( z89vMB_$@xq7x-2SnnKJyvH zeFF2s;D73~Pl5drMSA8l&wzPO%nP4+@iB&;XBS4`Dt0M$;6jC`j$Hg0*w3lm5}!k) z$i){F3EKhlB``0+^W~8Fieg1Bz5?bEtRZmm3Z6A0q8C4*+1NrBjsrUnuW)yjcY-qb z_zJdE;3|zUjSxqeMF=8<5h4gt1O_8XO6gPwxreTnrCa}e72sYNj02=_dLy>XfgxF z>vD0wlGiW-!@gEN9Nb&Rp?{dKRjLf1u2k8Fs6^(Axf69~*2V?exGXz?fXcI{5K;hF zF|=@%SLnS$?-hEl&}&m7GH6jYX3)YJO}NHB7-0`C=Q)4~uplTc#Dgr%p}wsU4~r;Z zgzPAxg3Ni8DLe)hipv=Bzy58>czJ+#5*MHtOEZ?K(J)?Q4%gAY^mY2~yHF3RV z3X41XcL9477DMolCI0el!F1m?Ewd+FSG?`JrsX{8eX#Gqn5VdMOjA4<2`NGU`%m)S zNEIe_4=0=e{6MNOVNsy~DugjgTbwXK=}Zz%5tBkZpJFkdhID2I=`5BHDkJG+O2ecy z#v10S$=!D>-Y^e^dH&8tt6DNT!ePmV32EH#_B*ajH_ZKRyJri>F}oeeIL$A%jdQ`K z8|Is;u$-!}kAy31B+kZ&1kASV*N)&ULuRqv>)Ni-am(Mu?G;FXy^64gu#T_+z#^qe zxnSSN{sV+z9Xx9bxov=#a5>8WV|gFsAKSmvxEnRVJMZD-KLY%3YrxY~j||~7;SAv{ z;T+ZM7U4YM0^x1KMZzV*JA})GcWLfD!WG2xdr;|BsPvl5c)~f^9j$Y$ak5K=j)UP< zIsuknYBc>?Pk}ioHq_{3ceL6m=wlSG+%wP{uQxK|1y5oGzv9!LJuErwtCQWD;75jY z$uM-m7NSEvdJ3yAhOVsDduLFOA|B&Az=8cm0kjV9b~Oj1r`YQGzt#-RF7|XkwfqO^j2d33{aKd7z0ghBPt8kS5eoO?aK5CcG}Ll)-yR zEp}Ydaq(58R_$)fv_#%;jnO{U5QGMc@(uz&pryyK$J@Bs^8o*amIey@vyBmVzLrYU z^BtC>a{|S#jq`DUkr?0nF0oW3=WD~DSu4# zPpevi;e=|rz-FP%ma2!@;TSp9>e_9?)w$VpW#ZVi`%SlR3p%(+#er9Uwp_{(|%D7v*kQwcjkD)0|#oW>xkiKxmwagcjZ*U>NX53YgyeUiyan9m0Y$YlOB5vV+MAq+q}ewIjI!$ zuor38>kiDi#AxrkCf9p1owtqipY?_P;Q%D~vEL~ff90L4zs5gK&bI*ZU_1m@FdmKs zQ=v%E|3Yu%8x95k!}o8>{9Ga#nwy4e9j?t}I0;elN?~;09<&+1KNtgiXo3sP1^*2q C(M2Wz literal 0 HcmV?d00001 diff --git a/clipette.py b/clipette.py deleted file mode 100644 index 391dd27..0000000 --- a/clipette.py +++ /dev/null @@ -1,550 +0,0 @@ -# must open clipboard before using any function -# must close clipboard afterwards - -import ctypes -from ctypes.wintypes import * -from os.path import join as path_join -from sys import getfilesystemencoding - -GMEM_MOVABLE = 2 -INT_P = ctypes.POINTER(ctypes.c_int) - -CF_UNICODETEXT = 13 -CF_HDROP = 15 -CF_BITMAP = 2 # hbitmap -CF_DIB = 8 # DIB and BITMAP are interconvertable as from windows clipboard -CF_DIBV5 = 17 - -# bitmap compression types -BI_RGB = 0 -BI_RLE8 = 1 -BI_RLE4 = 2 -BI_BITFIELDS = 3 -BI_JPEG = 4 -BI_PNG = 5 -BI_ALPHABITFIELDS = 6 - -format_dict = { - 1: 'CF_TEXT', - 2: 'CF_BITMAP', - 3: 'CF_METAFILEPICT', - 4: 'CF_SYLK', - 5: 'CF_DIF', - 6: 'CF_TIFF', - 7: 'CF_OEMTEXT', - 8: 'CF_DIB', - 9: 'CF_PALETTE', - 10: 'CF_PENDATA', - 11: 'CF_RIFF', - 12: 'CF_WAVE', - 13: 'CF_UNICODETEXT', - 14: 'CF_ENHMETAFILE', - 15: 'CF_HDROP', - 16: 'CF_LOCALE', - 17: 'CF_DIBV5', -} - -# todo: -# implement more formats (JPEG) -# write docs docs docs - -user32 = ctypes.windll.user32 -kernel32 = ctypes.windll.kernel32 -shell32 = ctypes.windll.shell32 - -user32.OpenClipboard.argtypes = HWND, -user32.OpenClipboard.restype = BOOL -user32.GetClipboardData.argtypes = UINT, -user32.GetClipboardData.restype = HANDLE -user32.SetClipboardData.argtypes = UINT, HANDLE -user32.SetClipboardData.restype = HANDLE -user32.CloseClipboard.argtypes = None -user32.CloseClipboard.restype = BOOL -user32.IsClipboardFormatAvailable.argtypes = UINT, -user32.IsClipboardFormatAvailable.restype = BOOL -user32.CountClipboardFormats.argtypes = None -user32.CountClipboardFormats.restype = UINT -user32.EnumClipboardFormats.argtypes = UINT, -user32.EnumClipboardFormats.restype = UINT -user32.GetClipboardFormatNameA.argtypes = UINT, LPSTR, UINT -user32.GetClipboardFormatNameA.restype = UINT -user32.RegisterClipboardFormatA.argtypes = LPCSTR, -user32.RegisterClipboardFormatA.restype = UINT -user32.RegisterClipboardFormatW.argtypes = LPCWSTR, -user32.RegisterClipboardFormatW.restype = UINT -user32.RegisterClipboardFormatW.argtypes = LPCWSTR, -user32.RegisterClipboardFormatW.restype = UINT -user32.EmptyClipboard.argtypes = None -user32.EmptyClipboard.restype = BOOL - -kernel32.GlobalAlloc.argtypes = UINT, ctypes.c_size_t -kernel32.GlobalAlloc.restype = HGLOBAL -kernel32.GlobalSize.argtypes = HGLOBAL, -kernel32.GlobalSize.restype = UINT -kernel32.GlobalLock.argtypes = HGLOBAL, -kernel32.GlobalLock.restype = LPVOID -kernel32.GlobalUnlock.argtypes = HGLOBAL, -kernel32.GlobalUnlock.restype = BOOL - -shell32.DragQueryFile.argtypes = HANDLE, UINT, ctypes.c_void_p, UINT -shell32.DragQueryFile.restype = UINT - - -class BITMAPFILEHEADER(ctypes.Structure): - _pack_ = 1 # structure field byte alignment - _fields_ = [ - ('bfType', WORD), # file type ("BM") - ('bfSize', DWORD), # file size in bytes - ('bfReserved1', WORD), # must be zero - ('bfReserved2', WORD), # must be zero - ('bfOffBits', DWORD), # byte offset to the pixel array - ] -sizeof_BITMAPFILEHEADER = ctypes.sizeof(BITMAPFILEHEADER) - -class BITMAPINFOHEADER(ctypes.Structure): - _pack_ = 1 # structure field byte alignment - _fields_ = [ - ('biSize', DWORD), - ('biWidth', LONG), - ('biHeight', LONG), - ('biPLanes', WORD), - ('biBitCount', WORD), - ('biCompression', DWORD), - ('biSizeImage', DWORD), - ('biXPelsPerMeter', LONG), - ('biYPelsPerMeter', LONG), - ('biClrUsed', DWORD), - ('biClrImportant', DWORD) - ] -sizeof_BITMAPINFOHEADER = ctypes.sizeof(BITMAPINFOHEADER) - -class BITMAPV4HEADER(ctypes.Structure): - _pack_ = 1 # structure field byte alignment - _fields_ = [ - ('bV4Size', DWORD), - ('bV4Width', LONG), - ('bV4Height', LONG), - ('bV4PLanes', WORD), - ('bV4BitCount', WORD), - ('bV4Compression', DWORD), - ('bV4SizeImage', DWORD), - ('bV4XPelsPerMeter', LONG), - ('bV4YPelsPerMeter', LONG), - ('bV4ClrUsed', DWORD), - ('bV4ClrImportant', DWORD), - ('bV4RedMask', DWORD), - ('bV4GreenMask', DWORD), - ('bV4BlueMask', DWORD), - ('bV4AlphaMask', DWORD), - ('bV4CSTypes', DWORD), - ('bV4RedEndpointX', LONG), - ('bV4RedEndpointY', LONG), - ('bV4RedEndpointZ', LONG), - ('bV4GreenEndpointX', LONG), - ('bV4GreenEndpointY', LONG), - ('bV4GreenEndpointZ', LONG), - ('bV4BlueEndpointX', LONG), - ('bV4BlueEndpointY', LONG), - ('bV4BlueEndpointZ', LONG), - ('bV4GammaRed', DWORD), - ('bV4GammaGreen', DWORD), - ('bV4GammaBlue', DWORD) - ] -sizeof_BITMAPV4HEADER = ctypes.sizeof(BITMAPV4HEADER) - -class BITMAPV5HEADER(ctypes.Structure): - _pack_ = 1 # structure field byte alignment - _fields_ = [ - ('bV5Size', DWORD), - ('bV5Width', LONG), - ('bV5Height', LONG), - ('bV5PLanes', WORD), - ('bV5BitCount', WORD), - ('bV5Compression', DWORD), - ('bV5SizeImage', DWORD), - ('bV5XPelsPerMeter', LONG), - ('bV5YPelsPerMeter', LONG), - ('bV5ClrUsed', DWORD), - ('bV5ClrImportant', DWORD), - ('bV5RedMask', DWORD), - ('bV5GreenMask', DWORD), - ('bV5BlueMask', DWORD), - ('bV5AlphaMask', DWORD), - ('bV5CSTypes', DWORD), - ('bV5RedEndpointX', LONG), - ('bV5RedEndpointY', LONG), - ('bV5RedEndpointZ', LONG), - ('bV5GreenEndpointX', LONG), - ('bV5GreenEndpointY', LONG), - ('bV5GreenEndpointZ', LONG), - ('bV5BlueEndpointX', LONG), - ('bV5BlueEndpointY', LONG), - ('bV5BlueEndpointZ', LONG), - ('bV5GammaRed', DWORD), - ('bV5GammaGreen', DWORD), - ('bV5GammaBlue', DWORD), - ('bV5Intent', DWORD), - ('bV5ProfileData', DWORD), - ('bV5ProfileSize', DWORD), - ('bV5Reserved', DWORD) - ] -sizeof_BITMAPV5HEADER = ctypes.sizeof(BITMAPV5HEADER) - -def open_clipboard(): - """ - Opens clipboard. Must be called before any action in performed. - - :return: (int) 0 if function fails, otherwise 1 - """ - return user32.OpenClipboard(0) - -def close_clipboard(): - """ - Closes clipboard. Must be called after all actions are performed. - - :return: (int) 0 if function fails, otherwise 1 - """ - return user32.CloseClipboard() - -def empty_cliboard(): - """ - Empties clipboard. Should be called before any setter actions. - - :return: (int) 0 if function fails, otherwise 1 - """ - return user32.EmptyClipboard() - -def get_UNICODETEXT(): - """ - get text from clipboard as string - - :return: (str) text grabbed from clipboard - """ - - # user32.OpenClipboard(0) - data = user32.GetClipboardData(CF_UNICODETEXT) - dest = kernel32.GlobalLock(data) - text = ctypes.wstring_at(dest) - kernel32.GlobalUnlock(data) - # user32.CloseClipboard() - - return text - -def set_UNICODETEXT(text): - """ - set text to clipboard as CF_UNICODETEXT - - :param str text: text to set to clipboard - :return: 1 if function succeeds, something else othewise (or maybe just spit out an error) - """ - - data = text.encode('utf-16le') - size = len(data) + 2 - - h_mem = kernel32.GlobalAlloc(GMEM_MOVABLE, size) - dest = kernel32.GlobalLock(h_mem) - ctypes.memmove(dest, data, size) - kernel32.GlobalUnlock(h_mem) - - # user32.OpenClipboard(0) - # user32.EmptyClipboard() - user32.SetClipboardData(CF_UNICODETEXT, h_mem) - # user32.CloseClipboard() - return 1 - -def get_FILEPATHS(): - """ - get list of files from clipboard. - - :return: (list) filepaths - """ - filepaths = [] - - #user32.OpenClipboard(0) - data = user32.GetClipboardData(CF_HDROP) - file_count = shell32.DragQueryFile(data, -1, None, 0) - for index in range(file_count): - buf = ctypes.c_buffer(260) - shell32.DragQueryFile(data, index, buf, ctypes.sizeof(buf)) - filepaths.append(buf.value.decode(getfilesystemencoding())) - #user32.CloseClipboard() - - return filepaths - -def get_DIB(filepath = '', filename = 'bitmap'): - """ - get image from clipboard as a bitmap and saves to filepath. - - :param str filepath: filepath to save image into - :param str filename: filename of the image - :return: 1 if function succeeds, something else othewise (or maybe just spit out an error) - """ - - # user32.OpenClipboard(0) - if not user32.IsClipboardFormatAvailable(CF_DIB): - raise_runtimerror("clipboard image not available in 'CF_DIB format") - - h_mem = user32.GetClipboardData(CF_DIB) - dest = kernel32.GlobalLock(h_mem) - size = kernel32.GlobalSize(dest) - data = bytes((ctypes.c_char*size).from_address(dest)) - - bm_ih = BITMAPINFOHEADER() - header_size = sizeof_BITMAPINFOHEADER - ctypes.memmove(ctypes.pointer(bm_ih), data, header_size) - - compression = bm_ih.biCompression - if compression not in (BI_BITFIELDS, BI_RGB): - raise_runtimerror(f'unsupported compression type {format(compression)}') - - bm_fh = BITMAPFILEHEADER() - ctypes.memset(ctypes.pointer(bm_fh), 0, sizeof_BITMAPFILEHEADER) - bm_fh.bfType = ord('B') | (ord('M') << 8) - bm_fh.bfSize = sizeof_BITMAPFILEHEADER + len(str(data)) - sizeof_COLORTABLE = 0 - bm_fh.bfOffBits = sizeof_BITMAPFILEHEADER + header_size + sizeof_COLORTABLE - - img_path = path_join(filepath, filename + '.bmp') - with open(img_path, 'wb') as bmp_file: - bmp_file.write(bm_fh) - bmp_file.write(data) - - kernel32.GlobalUnlock(h_mem) - # user32.CloseClipboard() - return 1 - -def get_DIBV5(filepath = '', filename = 'bitmapV5'): - """ - get image from clipboard as a bitmapV5 and saves to filepath - - :param str filepath: filepath to save image into - :param str filename: filename of the image - :return: 1 if function succeeds, something else othewise (or maybe just spit out an error) - """ - - # user32.OpenClipboard(0) - if not user32.IsClipboardFormatAvailable(CF_DIBV5): - raise_runtimerror("clipboard image not available in 'CF_DIBV5' format") - - h_mem = user32.GetClipboardData(CF_DIBV5) - dest = kernel32.GlobalLock(h_mem) - size = kernel32.GlobalSize(dest) - data = bytes((ctypes.c_char*size).from_address(dest)) - - bm_ih = BITMAPV5HEADER() - header_size = sizeof_BITMAPV5HEADER - ctypes.memmove(ctypes.pointer(bm_ih), data, header_size) - - if bm_ih.bV5Compression == BI_RGB: - # convert BI_RGB to BI_BITFIELDS so as to properly support an alpha channel - # everything other than the usage of bitmasks is same compared to BI_BITFIELDS so we manually add that part and put bV5Compression to BI_BITFIELDS - # info on these header structures -> https://docs.microsoft.com/en-us/windows/win32/gdi/bitmap-header-types - # and -> https://en.wikipedia.org/wiki/BMP_file_format - - bi_compression = bytes([3, 0, 0, 0]) - bi_bitmasks = bytes([0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 0, 0, 0, 0, 255]) - data = data[:16] + bi_compression + data[20:40] + bi_bitmasks + data[56:] - - elif bm_ih.bV5Compression == BI_BITFIELDS: - # we still need to add bitmask (bV5AlphaMask) for softwares to recognize the alpha channel - data = data[:52] + bytes([0, 0, 0, 255]) + data[56:] - - else: - raise_runtimerror(f'unsupported compression type {format(bm_ih.bV5Compression)}') - - bm_fh = BITMAPFILEHEADER() - ctypes.memset(ctypes.pointer(bm_fh), 0, sizeof_BITMAPFILEHEADER) - bm_fh.bfType = ord('B') | (ord('M') << 8) - bm_fh.bfSize = sizeof_BITMAPFILEHEADER + len(str(data)) - sizeof_COLORTABLE = 0 - bm_fh.bfOffBits = sizeof_BITMAPFILEHEADER + header_size + sizeof_COLORTABLE - - img_path = path_join(filepath, filename + '.bmp') - with open(img_path, 'wb') as bmp_file: - bmp_file.write(bm_fh) - bmp_file.write(data) - - kernel32.GlobalUnlock(h_mem) - # user32.CloseClipboard() - return 1 - -def get_PNG(filepath = '', filename = 'PNG'): - """ - get image in 'PNG' or 'image/png' format from clipboard and saves to filepath - - :param str filepath: filepath to save image into - :param str filename: filename of the image - :return: 1 if function succeeds, something else othewise (or maybe just spit out an error) - """ - - # user32.OpenClipboard(0) - png_format = 0 - PNG = user32.RegisterClipboardFormatW(ctypes.c_wchar_p('PNG')) - image_png = user32.RegisterClipboardFormatW(ctypes.c_wchar_p('image/png')) - if user32.IsClipboardFormatAvailable(PNG): - png_format = PNG - elif user32.IsClipboardFormatAvailable(image_png): - png_format = image_png - else: - raise_runtimerror("clipboard image not available in 'PNG' or 'image/png' format") - - h_mem = user32.GetClipboardData(png_format) - dest = kernel32.GlobalLock(h_mem) - size = kernel32.GlobalSize(dest) - data = bytes((ctypes.c_char*size).from_address(dest)) - kernel32.GlobalUnlock(h_mem) - # user32.CloseClipboard() - - img_path = path_join(filepath, filename + '.png') - with open (img_path, 'wb') as png_file: - png_file.write(data) - - return 1 - -def set_DIB(src_bmp): - """ - set source bitmap image to clipboard as a CF_DIB or CF_DIBV5 according to the image - - :param str src_bmp: filepath of source image - :return: 1 if function succeeds, something else othewise (or maybe just spit out an error) - """ - - with open(src_bmp, 'rb') as img: - data = img.read() - output = data[14:] - size = len(output) - print(list(bytearray(output)[:200])) - - mem = kernel32.GlobalAlloc(GMEM_MOVABLE, size) - h_mem = kernel32.GlobalLock(mem) - ctypes.memmove(ctypes.cast(h_mem, INT_P), ctypes.cast(output, INT_P), size) - kernel32.GlobalUnlock(mem) - - - if output[0] in [56, 108, 124]: - # img contains DIBV5 or DIBV4 or DIBV3 Header - fmt = CF_DIBV5 - else: - fmt = CF_DIB - - # user32.OpenClipboard(0) - # user32.EmptyClipboard() - user32.SetClipboardData(fmt, h_mem) - # user32.CloseClipboard() - return 1 - -def set_PNG(src_png): - """ - set source png image to clipboard in 'PNG' format - - :param str src_png: filepath of source image - :return: 1 if function succeeds, something else othewise (or maybe just spit out an error) - """ - with open(src_png, 'rb') as img: - data = img.read() - size = len(data) - - mem = kernel32.GlobalAlloc(GMEM_MOVABLE, size) - h_mem = kernel32.GlobalLock(mem) - ctypes.memmove(h_mem, data, size) - kernel32.GlobalUnlock(mem) - - # user32.OpenClipboard(0) - # user32.EmptyClipboard() - PNG = user32.RegisterClipboardFormatW(ctypes.c_wchar_p('PNG')) - user32.SetClipboardData(PNG, h_mem) - # user32.CloseClipboard() - return 1 - -def is_format_available(format_id): - """ - checks whether specified format is currently available on the clipboard - - :param int format_id: id of format to check for - :return: (bool) True if specified format is available - """ - - # user32.OpenClipboard(0) - is_format = user32.IsClipboardFormatAvailable(format_id) - # user32.CloseClipboard() - return bool(is_format) - -def get_available_formats(buffer_size = 32): - """ - gets a dict of all the currently available formats on the clipboard - - :param int buffer_size: (optional) buffer size to store name of each format in - :return: a dict {format_id : format_name} of all available formats - """ - available_formats = dict() - # user32.OpenClipboard(0) - fmt = 0 - for i in range(user32.CountClipboardFormats()): - # must put previous fmt (starting from 0) in EnumClipboardFormats() to get the next one - fmt = user32.EnumClipboardFormats(fmt) - name_buf = ctypes.create_string_buffer(buffer_size) - name_len = user32.GetClipboardFormatNameA(fmt, name_buf, buffer_size) - fmt_name = name_buf.value.decode() - - # standard formats do not return any name, so we set one from out dictionary - if fmt_name == '' and fmt in format_dict.keys(): - fmt_name = format_dict[fmt] - available_formats.update({fmt : fmt_name}) - - # user32.CloseClipboard() - return available_formats - -def get_image(filepath = '', filename = 'image'): - """ - gets image from clipboard in a format according to a priority list (PNG > DIBV5 > DIB) - - """ - # user32.OpenClipboard(0) - PNG = user32.RegisterClipboardFormatW(ctypes.c_wchar_p('PNG')) - image_png = user32.RegisterClipboardFormatW(ctypes.c_wchar_p('image/png')) - - if user32.IsClipboardFormatAvailable(PNG) or user32.IsClipboardFormatAvailable(image_png): - get_PNG(filepath, filename) - return 1 - elif user32.IsClipboardFormatAvailable(CF_DIBV5): - get_DIBV5(filepath, filename) - return 1 - elif user32.IsClipboardFormatAvailable(CF_DIB): - get_DIB(filepath, filename) - return 1 - else: - raise_runtimerror('image on clipboard not available in any supported format') - -def set_image(src_img): - """ - (NOT FULLY IMPLEMENTED) set source image to clipboard in multiple formats (PNG, DIB). - - :param str src_img: filepath of source image - :return: 1 if function succeeds, something else othewise (or maybe just spit out an error) - """ - # this is more complicated... gotta interconvert images - # looking into ways to get this done with ctypes as well - NO IM DONE - - # temporary solution - img_extn = src_img[(len(src_img)-3):].lower() - if img_extn == 'bmp': - # image format is bitmap - set_DIB(src_img) - elif img_extn == 'png': - # image format is png - set_PNG(src_img) - else: - raise_runtimerror('Unsupported image format') - - return 1 - -def raise_runtimerror(error_msg): - close_clipboard() - raise RuntimeError(error_msg) - -if __name__ == '__main__': - if open_clipboard(): - # empty_cliboard() - print(get_available_formats()) - # set_UNICODETEXT('pasta pasta pasta pasta pasta pasta') - close_clipboard() diff --git a/clipette/__init__.py b/clipette/__init__.py new file mode 100644 index 0000000..70871f7 --- /dev/null +++ b/clipette/__init__.py @@ -0,0 +1,417 @@ +""" +Clipette + +Python clipboard utility that works natively on python with its inbuilt +modules to exchange data with the windows clipboard. +Is designed particularly to work properly with different image formats. +Supports only the Windows clipboard through win32 API. + +Usage (setting): + import clipette + if clipette.open_clipboard(): + clipette.empty_cliboard() + clipette.set_UNICODETEXT("") + clipette.close_clipboard() + +Usage (getting): + import clipette + if cliptette.open_clipboard(): + clipette.get_PNG("", "filename") + clipette.close_clipboard() +""" + + +import ctypes +from os.path import join as path_join +from sys import getfilesystemencoding +import clipette.utils as utils + +def _raise_runtime_error(error_msg): + close_clipboard() + raise RuntimeError(error_msg) + +def _global_alloc(flags, size): + h_mem = utils.kernel32.GlobalAlloc(flags, size) + if(h_mem == None): + _raise_runtime_error("Unable to allocate memory.") + else: + return h_mem + +def _global_lock(h_mem): + lp_mem = utils.kernel32.GlobalLock(h_mem) + if(lp_mem == None): + _raise_runtime_error("Unable to lock global memory object.") + else: + return lp_mem + +def _global_unlock(h_mem): + utils.kernel32.GlobalUnlock(h_mem) + +def _get_clipboard_data(format): + h_mem = utils.user32.GetClipboardData(format) + if(h_mem == None): + _raise_runtime_error("Unable to access clipboard data.") + else: + return h_mem + +def _set_clipboard_data(format, h_mem): + h_data = utils.user32.SetClipboardData(format, h_mem) + if(h_data == None): + _raise_runtime_error("Unable to set clipboard data.") + else: + return h_data + + +def open_clipboard(): + """ + Opens clipboard. Must be called before any action in performed. + + :return: (int) 0 if function fails, otherwise 1 + """ + return utils.user32.OpenClipboard(None) + +def close_clipboard(): + """ + Closes clipboard. Must be called after all actions are performed. + + :return: (int) 0 if function fails, otherwise 1 + """ + return utils.user32.CloseClipboard() + +def empty_cliboard(): + """ + Empties clipboard. Should be called before any setter actions. + + :return: (int) 0 if function fails, otherwise 1 + """ + return utils.user32.EmptyClipboard() + +def get_UNICODETEXT(): + """ + get text from clipboard as string + + :return: (str) text grabbed from clipboard + """ + + # user32.OpenClipboard(0) + h_mem = _get_clipboard_data(utils.CF_UNICODETEXT) + lp_mem = _global_lock(h_mem) + text = ctypes.wstring_at(lp_mem) + _global_unlock(h_mem) + # user32.CloseClipboard() + + return text + +def set_UNICODETEXT(text): + """ + set text to clipboard as CF_UNICODETEXT + + :param str text: text to set to clipboard + :return: True if function succeeds + """ + + data = text.encode('utf-16le') + size = len(data) + 2 + + h_mem = _global_alloc(utils.GMEM_MOVABLE, size) + lp_mem = _global_lock(h_mem) + ctypes.memmove(lp_mem, data, size) + _global_unlock(h_mem) + + # user32.OpenClipboard(0) + # user32.EmptyClipboard() + _set_clipboard_data(utils.CF_UNICODETEXT, h_mem) + # user32.CloseClipboard() + return True + +def get_FILEPATHS(): + """ + get list of files from clipboard. + + :return: (list) filepaths + """ + filepaths = [] + + #user32.OpenClipboard(0) + h_mem = _get_clipboard_data(utils.CF_HDROP) + file_count = utils.shell32.DragQueryFile(h_mem, -1, None, 0) + for index in range(file_count): + buf = ctypes.c_buffer(260) + utils.shell32.DragQueryFile(h_mem, index, buf, ctypes.sizeof(buf)) + filepaths.append(buf.value.decode(getfilesystemencoding())) + #user32.CloseClipboard() + + return filepaths + +def get_DIB(filepath = '', filename = 'bitmap'): + """ + get image from clipboard as a bitmap and saves to filepath. + + :param str filepath: filepath to save image into + :param str filename: filename of the image + :return: full filepath of the saved image + """ + + # user32.OpenClipboard(0) + if not is_format_available(utils.CF_DIB): + _raise_runtime_error("clipboard image not available in 'CF_DIB' format") + + h_mem = _get_clipboard_data(utils.CF_DIB) + lp_mem = _global_lock(h_mem) + size = utils.kernel32.GlobalSize(lp_mem) + data = bytes((ctypes.c_char*size).from_address(lp_mem)) + + bm_ih = utils.BITMAPINFOHEADER() + header_size = utils.sizeof_BITMAPINFOHEADER + ctypes.memmove(ctypes.pointer(bm_ih), data, header_size) + + compression = bm_ih.biCompression + if compression not in (utils.BI_BITFIELDS, utils.BI_RGB): + _raise_runtime_error(f'unsupported compression type {format(compression)}') + + bm_fh = utils.BITMAPFILEHEADER() + ctypes.memset(ctypes.pointer(bm_fh), 0, utils.sizeof_BITMAPFILEHEADER) + bm_fh.bfType = ord('B') | (ord('M') << 8) + bm_fh.bfSize = utils.sizeof_BITMAPFILEHEADER + len(str(data)) + sizeof_COLORTABLE = 0 + bm_fh.bfOffBits = utils.sizeof_BITMAPFILEHEADER + header_size + sizeof_COLORTABLE + + img_path = path_join(filepath, filename + '.bmp') + with open(img_path, 'wb') as bmp_file: + bmp_file.write(bm_fh) + bmp_file.write(data) + + _global_unlock(h_mem) + # user32.CloseClipboard() + return img_path + +def get_DIBV5(filepath = '', filename = 'bitmapV5'): + """ + get image from clipboard as a bitmapV5 and saves to filepath + + :param str filepath: filepath to save image into + :param str filename: filename of the image + :return: full filepath of the saved image + """ + + # user32.OpenClipboard(0) + if not is_format_available(utils.CF_DIBV5): + _raise_runtime_error("clipboard image not available in 'CF_DIBV5' format") + + h_mem = _get_clipboard_data(utils.CF_DIBV5) + lp_mem = _global_lock(h_mem) + size = utils.kernel32.GlobalSize(lp_mem) + data = bytes((ctypes.c_char*size).from_address(lp_mem)) + + bm_ih = utils.BITMAPV5HEADER() + header_size = utils.sizeof_BITMAPV5HEADER + ctypes.memmove(ctypes.pointer(bm_ih), data, header_size) + + if bm_ih.bV5Compression == utils.BI_RGB: + # convert BI_RGB to BI_BITFIELDS so as to properly support an alpha channel + # everything other than the usage of bitmasks is same compared to BI_BITFIELDS so we manually add that part and put bV5Compression to BI_BITFIELDS + # info on these header structures -> https://docs.microsoft.com/en-us/windows/win32/gdi/bitmap-header-types + # and -> https://en.wikipedia.org/wiki/BMP_file_format + + bi_compression = bytes([3, 0, 0, 0]) + bi_bitmasks = bytes([0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 0, 0, 0, 0, 255]) + data = data[:16] + bi_compression + data[20:40] + bi_bitmasks + data[56:] + + elif bm_ih.bV5Compression == utils.BI_BITFIELDS: + # we still need to add bitmask (bV5AlphaMask) for softwares to recognize the alpha channel + data = data[:52] + bytes([0, 0, 0, 255]) + data[56:] + + else: + _raise_runtime_error(f'unsupported compression type {format(bm_ih.bV5Compression)}') + + bm_fh = utils.BITMAPFILEHEADER() + ctypes.memset(ctypes.pointer(bm_fh), 0, utils.sizeof_BITMAPFILEHEADER) + bm_fh.bfType = ord('B') | (ord('M') << 8) + bm_fh.bfSize = utils.sizeof_BITMAPFILEHEADER + len(str(data)) + sizeof_COLORTABLE = 0 + bm_fh.bfOffBits = utils.sizeof_BITMAPFILEHEADER + header_size + sizeof_COLORTABLE + + img_path = path_join(filepath, filename + '.bmp') + with open(img_path, 'wb') as bmp_file: + bmp_file.write(bm_fh) + bmp_file.write(data) + + _global_unlock(h_mem) + # user32.CloseClipboard() + return img_path + +def get_PNG(filepath = '', filename = 'PNG'): + """ + get image in 'PNG' or 'image/png' format from clipboard and saves to filepath + + :param str filepath: filepath to save image into + :param str filename: filename of the image + :return: full filepath of the saved image + """ + + # user32.OpenClipboard(0) + png_format = 0 + PNG = utils.user32.RegisterClipboardFormatW(ctypes.c_wchar_p('PNG')) + image_png = utils.user32.RegisterClipboardFormatW(ctypes.c_wchar_p('image/png')) + if utils.user32.IsClipboardFormatAvailable(PNG): + png_format = PNG + elif utils.user32.IsClipboardFormatAvailable(image_png): + png_format = image_png + else: + _raise_runtime_error("clipboard image not available in 'PNG' or 'image/png' format") + + h_mem = _get_clipboard_data(png_format) + lp_mem = _global_lock(h_mem) + size = utils.kernel32.GlobalSize(lp_mem) + data = bytes((ctypes.c_char*size).from_address(lp_mem)) + _global_unlock(h_mem) + # user32.CloseClipboard() + + img_path = path_join(filepath, filename + '.png') + with open (img_path, 'wb') as png_file: + png_file.write(data) + + return img_path + +def set_DIB(src_bmp): + """ + set source bitmap image to clipboard as a CF_DIB or CF_DIBV5 according to the image + + :param str src_bmp: full filepath of source image + :return: True if function succeeds + """ + + with open(src_bmp, 'rb') as img: + data = img.read() + output = data[14:] + size = len(output) + print(list(bytearray(output)[:200])) + + h_mem = _global_alloc(utils.GMEM_MOVABLE, size) + lp_mem = _global_lock(h_mem) + ctypes.memmove(ctypes.cast(lp_mem, utils.INT_P), ctypes.cast(output, utils.INT_P), size) + _global_unlock(h_mem) + + if output[0] in [56, 108, 124]: + # img contains DIBV5 or DIBV4 or DIBV3 Header + fmt = utils.CF_DIBV5 + else: + fmt = utils.CF_DIB + + # user32.OpenClipboard(0) + # user32.EmptyClipboard() + _set_clipboard_data(fmt, lp_mem) + # user32.CloseClipboard() + return 1 + +def set_PNG(src_png): + """ + set source png image to clipboard in 'PNG' format + + :param str src_png: full filepath of source image + :return: True if function succeeds + """ + with open(src_png, 'rb') as img: + data = img.read() + size = len(data) + + h_mem = _global_alloc(utils.GMEM_MOVABLE, size) + lp_mem = _global_lock(h_mem) + ctypes.memmove(lp_mem, data, size) + _global_unlock(h_mem) + + # user32.OpenClipboard(0) + # user32.EmptyClipboard() + PNG = utils.user32.RegisterClipboardFormatW(ctypes.c_wchar_p('PNG')) + _set_clipboard_data(PNG, lp_mem) + # user32.CloseClipboard() + return True + +def is_format_available(format_id): + """ + checks whether specified format is currently available on the clipboard + + :param int format_id: id of format to check for + :return: (bool) True if specified format is available, False otherwise. + """ + + # user32.OpenClipboard(0) + is_format = utils.user32.IsClipboardFormatAvailable(format_id) + # user32.CloseClipboard() + return bool(is_format) + +def get_available_formats(buffer_size = 32): + """ + gets a dict of all the currently available formats on the clipboard + + :param int buffer_size: (optional) buffer size to store name of each format in + :return: a dict {format_id : format_name} of all available formats + """ + available_formats = dict() + # user32.OpenClipboard(0) + fmt = 0 + for i in range(utils.user32.CountClipboardFormats()): + # must put previous fmt (starting from 0) in EnumClipboardFormats() to get the next one + fmt = utils.user32.EnumClipboardFormats(fmt) + name_buf = ctypes.create_string_buffer(buffer_size) + name_len = utils.user32.GetClipboardFormatNameA(fmt, name_buf, buffer_size) + fmt_name = name_buf.value.decode() + + # standard formats do not return any name, so we set one from out dictionary + if fmt_name == '' and fmt in utils.format_dict.keys(): + fmt_name = utils.format_dict[fmt] + available_formats.update({fmt : fmt_name}) + + # user32.CloseClipboard() + return available_formats + +def get_image(filepath = '', filename = 'image'): + """ + gets image from clipboard in a format according to a priority list (PNG > DIBV5 > DIB) + + :param str filepath: filepath to save image into + :param str filename: filename of the image + :return: full filepath of the saved image + """ + # user32.OpenClipboard(0) + PNG = utils.user32.RegisterClipboardFormatW(ctypes.c_wchar_p('PNG')) + image_png = utils.user32.RegisterClipboardFormatW(ctypes.c_wchar_p('image/png')) + + if utils.user32.IsClipboardFormatAvailable(PNG) or utils.user32.IsClipboardFormatAvailable(image_png): + return get_PNG(filepath, filename) + elif utils.user32.IsClipboardFormatAvailable(utils.CF_DIBV5): + return get_DIBV5(filepath, filename) + elif utils.user32.IsClipboardFormatAvailable(utils.CF_DIB): + return get_DIB(filepath, filename) + else: + _raise_runtime_error('image on clipboard not available in any supported format') + +def set_image(src_img): + """ + (NOT FULLY IMPLEMENTED) set source image to clipboard in multiple formats (PNG, DIB). + + :param str src_img: full filepath of source image + :return: True if function succeeds + """ + # this is more complicated... gotta interconvert images + # looking into ways to get this done with ctypes as well - NO IM DONE + + # temporary solution + img_extn = src_img[(len(src_img)-3):].lower() + if img_extn == 'bmp': + set_DIB(src_img) + elif img_extn == 'png': + set_PNG(src_img) + else: + _raise_runtime_error('Unsupported image format') + + return True + + + +if __name__ == '__main__': + if open_clipboard(): + # empty_cliboard() + # print(get_available_formats()) + set_UNICODETEXT('pasta pasta') + close_clipboard() diff --git a/clipette/__pycache__/__init__.cpython-39.pyc b/clipette/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3e0aac62b1bca47e16d836866b991cd73c2a4c5 GIT binary patch literal 11137 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") + clipette.close_clipboard() \ No newline at end of file