本文章是第一次更新,更新於 2022年3月28日
准備工作😀
方案設想
方案設想來自2019年的文章 使用 Python 對接 PicGo,實現自動添加水印並上傳 (寫代碼的明哥)
此方案部分代碼已過時,由於pyclipboard
庫不支持windows
,遂做出修改。編寫一個能在windows
上使用的剪貼板圖片自動加水印工具,即按下快捷鍵自動給剪貼板(復制)的圖片添加水印。 改動如下
- 使用最新的
Pynput
全局快捷鍵監聽器 - 使用
win32clipboard
對windows
剪貼板進行操作 - 使用
plyer
進行windows
彈窗通知 - 使用
pyinstaller
進行exe包裝 - 其他細節上的改動
PS:我踩了很多坑,這些坑都被記錄下來,幫你們填完。
官方文檔
下面是我參考過的文檔
- https://pynput.readthedocs.io/en/latest/index.html
- https://plyer.readthedocs.io/en/latest/#plyer.facades.Notification
- https://pillow.readthedocs.io/en/stable/reference/ImageFont.html
- https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html
- https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html#text-anchors
- https://pillow.readthedocs.io/en/stable/reference/ImageGrab.html
- https://docs.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats
- https://pyinstaller.readthedocs.io/en/stable/
資料下載
如果文章字體過小,請調整瀏覽器頁面縮放。Windows: Ctrl + 鼠標滾輪
本篇文章代碼注釋使用了 vscode 的 better-comments 拓展
軟件效果
原圖片
修改后
按下快捷鍵后,你可以看到四層水印,分別在左上,左下,中間,右下。
開發過程
功能分割
我將程序分為了三個部分,通知處理模塊Notify.py
,圖片處理模塊Image.py
,鍵盤監聽模塊Work.py
notify.py
具體功能
用於右下角的系統通知
Plyer
-
Install the
plyer
module withpip
.pip install plyer
-
plyer
comes with a class callednotification
, which helps us create a notification. Import it like this:from plyer import notification
-
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._MEIPASS
,os.path.abspath(".")
通過絕對路徑方法獲取當前目錄位置。
注意:執行exe時 os.path.dirname(__file__)
將會出錯無法得到正確路徑,只能用於本地測試。
image.py
具體功能
- 從剪貼板獲取圖片
- 處理圖片
- 將圖片放回剪貼板
PIL 庫使用
PIL 是 Python 的圖像處理標准庫
官方文檔
- https://pillow.readthedocs.io/en/stable/reference/ImageFont.html 字體的設置
- https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html 圖像的處理,比如添加字體
- https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html#text-anchors 添加字體錨點
- https://pillow.readthedocs.io/en/stable/reference/ImageGrab.html 從剪貼板取圖片數據
其他資料
https://www.liaoxuefeng.com/wiki/1016959663602400/1017785454949568 PIL介紹與使用
https://blog.csdn.net/weixin_43790276/article/details/108478270 PIL簡單圖片處理
圖片存入剪貼板
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
安裝 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
執行命令
在當前終端執行如下命令
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!
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
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()