Python Windows 快捷鍵自動給剪貼板(復制)圖片添加水印


本文章是第一次更新,更新於 2022年3月28日


准備工作😀

方案設想

方案設想來自2019年的文章 使用 Python 對接 PicGo,實現自動添加水印並上傳 (寫代碼的明哥)

此方案部分代碼已過時,由於pyclipboard庫不支持windows,遂做出修改。編寫一個能在windows上使用的剪貼板圖片自動加水印工具,即按下快捷鍵自動給剪貼板(復制)的圖片添加水印。 改動如下

  • 使用最新的Pynput 全局快捷鍵監聽器
  • 使用 win32clipboardwindows剪貼板進行操作
  • 使用plyer進行windows彈窗通知
  • 使用pyinstaller進行exe包裝
  • 其他細節上的改動

PS:我踩了很多坑,這些坑都被記錄下來,幫你們填完。

官方文檔

下面是我參考過的文檔

資料下載

代碼:AutoWatermark.zip

image-20220329001724058

如果文章字體過小,請調整瀏覽器頁面縮放。Windows: Ctrl + 鼠標滾輪

本篇文章代碼注釋使用了 vscode 的 better-comments 拓展


軟件效果

原圖片

image-20220328163050872

修改后

按下快捷鍵后,你可以看到四層水印,分別在左上,左下,中間,右下。

image-20220328163405794

image-20220328163351139


開發過程

功能分割

我將程序分為了三個部分,通知處理模塊Notify.py,圖片處理模塊Image.py,鍵盤監聽模塊Work.py

graph TB A(AutoWatermark.exe) --- Notify[Notify.py] A --- Image[Image.py] Notify --- Work(Work.py) Image --- Work

notify.py

具體功能

用於右下角的系統通知

image-20220328160708407

Plyer

  1. Install the plyer module with pip.

    pip install plyer
    
  2. plyer comes with a class called notification, which helps us create a notification. Import it like this:

    from plyer import notification
    
  3. For Example

#import notification from plyer module 導入 player 里的 notification 方法
from plyer import notification
#import time 導入時間庫
import time

#Use while loop to create notifications indefinetly 死循環
while(True):
    #notification 進行系統提示
    notification.notify(
        title = "Reminder to take a break",
        message = '''Drink water, take a walk''',
        timeout = 60
    )
    #System pause the execution of this programm for 60 minutes 每60分鍾執行一次
    time.sleep(60*60)
Windows有效的可選參數:

title (str): 顯示通知標題

message (str): 顯示通知信息

app_icon (str): 顯示通知圖標,注意windows只能使用ico

timeout (int): 通知顯示時間

notify.py

notification進行簡單封裝

from plyer import notification
import os
import random
import sys

path = os.path.dirname(__file__)

appname = "自動水印"

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    try:
        # PyInstaller creates a temp folder and stores path in _MEIPASS
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)


def notify(title, message, icon):
    notification.notify(
        title=title,
        message=message,
        app_name=appname,
        app_icon=resource_path('{}.ico'.format(icon)),
        timeout=2
    )

代碼當中有一個 resource_path方法,這個是取當前文件夾路徑,是為了打包成exe之后能夠使用依賴資源,因為使用的不是exe當前的目錄,而是temp緩存目錄,pyinstaller打包出來的可執行程序會將temp目錄保存到 sys._MEIPASSos.path.abspath(".")通過絕對路徑方法獲取當前目錄位置。

注意:執行exe時 os.path.dirname(__file__) 將會出錯無法得到正確路徑,只能用於本地測試。


image.py

具體功能

  • 從剪貼板獲取圖片
  • 處理圖片
  • 將圖片放回剪貼板

PIL 庫使用

PIL 是 Python 的圖像處理標准庫

官方文檔

其他資料

https://www.liaoxuefeng.com/wiki/1016959663602400/1017785454949568 PIL介紹與使用

https://blog.csdn.net/weixin_43790276/article/details/108478270 PIL簡單圖片處理

圖片存入剪貼板

https://stackoverflow.com/questions/7050448/write-image-to-windows-clipboard-in-python-with-pil-and-win32clipboard

Windows 剪貼板僅支持 BMP 位圖格式,所以我們需要對Image進行格式轉換。存入剪貼板的數據需要去除BMP格式的頭部信息(位圖信息)。

圖片轉化后,我們需要一組二進制字節流,用於存儲圖片的數據,再從二進制流里讀取數據放入剪貼板中。

    img_byte_arr = io.BytesIO()  # 生成二進制字節流
    img.save(img_byte_arr, format='BMP')  # 將圖片輸入二進制字節流中
    img_byte_arr = img_byte_arr.getvalue()[14:]  # 取出二進制字節流並切片截取

存入剪貼板需要使用 win32clipboard 庫,需要 pip install pywin32

如何畫粗體字

https://stackoverflow.com/questions/1815165/draw-bold-italic-text-with-pil

添加文字框的方法中發現並沒有字體變粗的屬性,具體實現有兩種

  • 切換字體,像微軟雅黑UI 有三種字體,分別是 粗、普通、細,文件名也不同

  • 描邊,對 drawTXT.text 添加參數,最好顏色一致,這種可以自定義字體粗的程度

        drawTXT.text((x/2, y/2), "xiaoneng1024", font=font, fill=(220, 220, 220, 16), anchor='mm',stroke_width=2,stroke_fill = "black")
    

image.py

import io
import os
import random
import win32clipboard
import time
from PIL import Image, ImageFont, ImageDraw, ImageGrab
from notify import notify


path = os.path.dirname(__file__)

# img = Image.open(path + "/lazy.jpeg") # ^ 測試用


def main():
    # ^ 運行主流程
    img = getClipboard()
    if img == None:
        notify('AutoWatermark', '剪貼板不是圖片數據', 'warn')
        return
    # print(img.size, img.mode)
    try:
        img = imgProcessing(img)
        img_bytes = imgToBytes(img)
        res = setClipboard(img_bytes)
    except:
        notify('AutoWatermark', '異常錯誤 ERROR', 'file')
        return
    if res == True:
        notify('AutoWatermark', '圖片已自動添加水印', 'file')
    else:
        notify('AutoWatermark', '剪貼板讀取失敗:拒絕訪問', 'warn')


def getClipboard():
    # ^ 獲取剪貼板數據
    try:
        img = ImageGrab.grabclipboard()
    except:
        return None
    return img


def imgProcessing(img):
    # ^ 圖像處理
    # @ 將圖像轉換為RGBA,添加通道值方便后期與字體圖層疊加
    img = img.convert("RGBA")
    x, y = img.size
    fontsize = 18
    # @ 根據截取大小調整字體大小
    if x*y > 1280 * 720:
        fontsize = 28
    elif x*y > 1600 * 900:
        fontsize = 36
    elif x*y > 1920 * 1080:
        fontsize = 48
    # @ 選擇字體,這里我選擇微軟雅黑
    font = ImageFont.truetype("msyh.ttc", fontsize)
    # @ 新建一個存文字框的圖層
    txt = Image.new('RGBA', img.size, (0, 0, 0, 0))
    # @ 創建Draw對象,可以對圖層繪圖操作
    drawTXT = ImageDraw.Draw(txt)
    drawTXT.text((x-4, y-4), "cnblogs.com/linxiaoxu", font=font, fill=(220, 220, 220, 156), anchor='rs')  # stroke_width=2,stroke_fill = "black"
    drawTXT.text((x/2, y/2), "xiaoneng1024", font=font, fill=(220, 220, 220, 16), anchor='mm')
    drawTXT.text((4, 4), time.strftime("%Y-%m-%d", time.gmtime()), font=font, fill=(220, 220, 220, 24), anchor='la')  # stroke_width=2,stroke_fill = "black"
    drawTXT.text((4, y), "小能1024", font=font, fill=(220, 220, 220, 24), anchor='lb')
    # @ 將兩個圖層合並
    img = Image.alpha_composite(img, txt)
    return img


def imgToBytes(img):
    # ^ 將圖像轉換為二進制流,並裁剪頭部信息
    img_byte_arr = io.BytesIO()
    img.save(img_byte_arr, format='BMP')
    img_byte_arr = img_byte_arr.getvalue()[14:]
    # print(img_byte_arr, type(img_byte_arr))
    return img_byte_arr


def setClipboard(img_byte_arr):
    # ^ 設置系統剪貼板數據
    try:
        win32clipboard.OpenClipboard()
        win32clipboard.EmptyClipboard()
        win32clipboard.SetClipboardData(win32clipboard.CF_DIB, img_byte_arr)
        win32clipboard.CloseClipboard()
        return True
    except:
        return False


if __name__ == '__main__':
    main()

work.py

pynput 包

我們需要有一個對全局鍵盤事件進行監聽並響應事件的線程,這時 pynput 包作用就出來了,用於控制和監視鼠標、鍵盤的類

https://pynput.readthedocs.io/en/latest/index.html

work.py

from pynput import keyboard
from image import main
from notify import notify


def on_activate():
    print('Global hotkey activated!')
    main()


def for_canonical(f):
    return lambda k: f(l.canonical(k))


notify('AutoWatermark', '程序已啟動!', 'warn')

hotkey = keyboard.HotKey(
    keyboard.HotKey.parse('<ctrl>+<shift>+,'),
    on_activate)
with keyboard.Listener(
        on_press=for_canonical(hotkey.press),
        on_release=for_canonical(hotkey.release)) as l:
    l.join()

on_activate 是觸發快捷鍵的方法

for_canonical(f) 是修飾器,返回一個匿名函數,為了調用 listener 的方法 canonical 使用戶輸入的按鍵規范化

奇數行是直接調用,不使用修飾器。偶數行使用修飾器,可以看到原本的 Key.alt_l 被規范成 Key.alt

第1次 Key.alt_l <enum 'Key'>
第1次 Key.alt <enum 'Key'>
第2次 Key.ctrl_l <enum 'Key'>
第2次 Key.ctrl <enum 'Key'>
hotkey = keyboard.HotKey(
    keyboard.HotKey.parse('<ctrl>+<shift>+,'),
    on_activate)

這個是設置HotKey對象,parse方法方便轉換組合的快捷鍵,on_activate 是觸發時調用的方法

with keyboard.Listener(
        on_press=for_canonical(hotkey.press),
        on_release=for_canonical(hotkey.release)) as l:
    l.join()

這個是阻塞式的用法,開始一個新線程監聽鍵盤事件


打包exe

image-20220328230256628

安裝 pyinstaller

pip install pyinstaller

[已解決] ModuleNotFoundError: No module named ‘pip‘

在安裝 pyinstaller 的過程中控制台報錯,ModuleNotFoundError: No module named ‘pip‘

解決方法

python -m ensurepip
python -m pip install --upgrade pip

一個py打包exe

合並py

我們將三個py合並為一個py

image-20220328163953727

執行命令

在當前終端執行如下命令

pyinstaller --windowed  --icon cat.ico -i cat.ico --onefile --add-data "L:/IT/Python/Codes/Work/220327_AutoWatermark/file.ico;." --add-data "L:/IT/Python/Codes/Work/220327_AutoWatermark/warn.ico;." --hidden-import plyer.platforms.win.notification --hidden-import PIL final.py

參數介紹

  • --windowed 最小化運行
  • --icon 設置圖表
  • --onefile 生成一個exe文件
  • --add-data 添加第三方依賴
  • --hidden-import 手動添加需要打包的庫
  • final.py 當前需要打包的py腳本

多個py打包exe

執行命令

在當前終端執行如下命令

pyinstaller --windowed  --icon cat.ico -i cat.ico --onefile --add-data "L:/IT/Python/Codes/Work/220327_AutoWatermark/file.ico;." --add-data "L:/IT/Python/Codes/Work/220327_AutoWatermark/warn.ico;." --hidden-import plyer.platforms.win.notification --hidden-import PIL work.py -p notify.py -p image.py

參數介紹

  • -p 其他腳本

打包錯誤

Plyer 庫未被打包導致方法調用失敗

Traceback (most recent call last):
    ...
ModuleNotFoundError: No module named 'plyer.platforms'
Traceback (most recent call last):
  File "notification test.py", line 3, in <module>
  File "plyer\facades\notification.py", line 79, in notify
  File "plyer\facades\notification.py", line 88, in _notify
NotImplementedError: No usable implementation found!
[12520] Failed to execute script 'notification test' due to unhandled exception! 

plyer notification in Python?

https://stackoverflow.com/questions/67489963/failed-to-execute-script-pyinstaller

plyer 模塊不會自動被打包,需要手動添加命令 --hidden-import plyer.platforms.win.notification

另一種方法

PIL庫也可能未被打包,先打包一次,在當前目錄下找到被打包py的spec文件,打開修改添加庫所在的位置。我是win10,庫文件夾目錄在C:\Users\Administrator\AppData\Local\Programs\Python\Python310\Lib\site-packages

image-20220328232420578

datas=[('C:/Users/Administrator/AppData/Local/Programs/Python/Python310/Lib/site-packages/PIL','PIL'),],

其他資料

https://pypi.org/project/auto-py-to-exe/ 打包工具

https://www.imooc.com/article/286538 打包指南

https://www.zhihu.com/question/281858271/answer/611320245 打包文件太大了

final.py

import os
import io
import win32clipboard
import time
import sys
from PIL import Image, ImageFont, ImageDraw, ImageGrab
from pynput import keyboard
from plyer import notification

# path = os.path.dirname(__file__)
appname = "自動水印"


def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    try:
        # PyInstaller creates a temp folder and stores path in _MEIPASS
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)


def notify(title, message, icon):
    notification.notify(
        title=title,
        message=message,
        app_name=appname,
        app_icon=resource_path('{}.ico'.format(icon)),
        timeout=19
    )


def main():
    img = getClipboard()
    if img == None:
        notify('AutoWatermark', '剪貼板不是圖片數據', 'warn')
        return
    # print(img.size, img.mode)
    try:
        img = imgProcessing(img)
        img_bytes = imgToBytes(img)
        res = setClipboard(img_bytes)
    except:
        notify('AutoWatermark', '異常錯誤 ERROR', 'file')
        return
    if res == True:
        notify('AutoWatermark', '圖片已自動添加水印', 'file')
    else:
        notify('AutoWatermark', '剪貼板讀取失敗:拒絕訪問', 'warn')


def getClipboard():
    try:
        img = ImageGrab.grabclipboard()
    except:
        return None
    return img


def imgProcessing(img):
    img = img.convert("RGBA")
    x, y = img.size
    fontsize = 18
    if x*y > 1280 * 720:
        fontsize = 28
    elif x*y > 1600 * 900:
        fontsize = 36
    elif x*y > 1920 * 1080:
        fontsize = 48
    font = ImageFont.truetype("msyh.ttc", fontsize)
    txt = Image.new('RGBA', img.size, (0, 0, 0, 0))
    drawTXT = ImageDraw.Draw(txt)
    drawTXT.text((x-4, y-4), "cnblogs.com/linxiaoxu", font=font, fill=(220, 220, 220, 156), anchor='rs')  # stroke_width=2,stroke_fill = "black"
    drawTXT.text((x/2, y/2), "xiaoneng1024", font=font, fill=(220, 220, 220, 16), anchor='mm')
    drawTXT.text((4, 4), time.strftime("%Y-%m-%d", time.gmtime()), font=font, fill=(220, 220, 220, 24), anchor='la')  # stroke_width=2,stroke_fill = "black"
    drawTXT.text((4, y), "小能1024", font=font, fill=(220, 220, 220, 24), anchor='lb')
    img = Image.alpha_composite(img, txt)
    return img


def imgToBytes(img):
    img_byte_arr = io.BytesIO()
    img.save(img_byte_arr, format='BMP')
    img_byte_arr = img_byte_arr.getvalue()[14:]
    # print(img_byte_arr, type(img_byte_arr))
    return img_byte_arr


def setClipboard(img_byte_arr):
    try:
        win32clipboard.OpenClipboard()
        win32clipboard.EmptyClipboard()
        win32clipboard.SetClipboardData(win32clipboard.CF_DIB, img_byte_arr)
        win32clipboard.CloseClipboard()
        return True
    except:
        return False


def on_activate():
    print('Global hotkey activated!')
    main()


def for_canonical(f):
    return lambda k: f(l.canonical(k))


notify('AutoWatermark', '程序已啟動!', 'warn')

hotkey = keyboard.HotKey(
    keyboard.HotKey.parse('<ctrl>+<shift>+,'),
    on_activate)
with keyboard.Listener(
        on_press=for_canonical(hotkey.press),
        on_release=for_canonical(hotkey.release)) as l:
    l.join()


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM