Python基于wxPython開發(fā)一個圖片PDF生成器
前言
本文將詳細解析一個基于wxPython開發(fā)的圖片PDF生成器應(yīng)用程序。該程序能夠批量處理圖片,支持旋轉(zhuǎn)、剪切等編輯功能,并按照指定順序?qū)D片導(dǎo)出為PDF文件。通過本文,你將學(xué)習(xí)到wxPython GUI開發(fā)、PIL圖像處理、ReportLab PDF生成等技術(shù)要點。
C:\pythoncode\new\pics2pdf.py
項目概述
功能特性
- 圖片管理:瀏覽文件夾并加載所有圖片文件
- 圖片預(yù)覽:實時預(yù)覽選中的圖片
- 圖片編輯:
- 旋轉(zhuǎn)功能(支持90度左旋/右旋)
- 剪切功能(鼠標拖拽選擇區(qū)域)
- 重置功能(恢復(fù)原始狀態(tài))
- 列表管理:在兩個列表間移動圖片,調(diào)整順序
- PDF導(dǎo)出:按指定順序?qū)D片導(dǎo)出為PDF文件
技術(shù)棧
- wxPython:GUI界面框架
- PIL/Pillow:圖像處理庫
- ReportLab:PDF生成庫
- Python標準庫:文件操作、數(shù)據(jù)結(jié)構(gòu)
代碼架構(gòu)分析
整體結(jié)構(gòu)
程序采用面向?qū)ο笤O(shè)計,主要包含兩個類:
ImagePDFFrame:主窗口類,負責(zé)整體UI和核心功能CropDialog:剪切對話框類,負責(zé)圖片剪切功能
ImagePDFFrame (主窗口)
├── UI布局
│ ├── 左側(cè):圖片列表 + 選擇文件夾按鈕
│ ├── 中間:添加/移除按鈕
│ ├── 右側(cè):待導(dǎo)出列表 + 順序調(diào)整按鈕
│ └── 預(yù)覽區(qū):圖片顯示 + 編輯按鈕
├── 數(shù)據(jù)管理
│ ├── image_folder:當前文件夾路徑
│ ├── image_files:圖片文件列表
│ ├── image_modifications:圖片編輯記錄
│ └── current_preview_image:當前預(yù)覽圖片
└── 功能模塊
├── 文件加載
├── 圖片預(yù)覽
├── 圖片編輯
└── PDF生成
核心代碼詳解
1. 初始化與UI布局
def __init__(self):
super().__init__(None, title="圖片PDF生成器", size=(1400, 800))
self.image_folder = ""
self.image_files = []
self.current_preview_image = None
self.current_preview_filename = None
self.image_modifications = {} # 關(guān)鍵數(shù)據(jù)結(jié)構(gòu):存儲圖片編輯信息
數(shù)據(jù)結(jié)構(gòu)設(shè)計亮點:
image_modifications 字典的設(shè)計非常巧妙:
{
'photo1.jpg': {
'rotation': 90, # 旋轉(zhuǎn)角度(0, 90, 180, 270)
'crop': (x1, y1, x2, y2) # 剪切區(qū)域坐標
},
'photo2.jpg': {
'rotation': 0,
'crop': None
}
}
這種設(shè)計的優(yōu)勢:
- 每張圖片的編輯狀態(tài)獨立存儲
- 可隨時查詢和應(yīng)用編輯
- 支持撤銷和重置操作
2. UI布局:BoxSizer的應(yīng)用
wxPython使用Sizer進行布局管理,代碼中使用了wx.BoxSizer:
main_sizer = wx.BoxSizer(wx.HORIZONTAL) # 水平主布局 # 左側(cè)垂直布局 left_sizer = wx.BoxSizer(wx.VERTICAL) left_sizer.Add(btn_select, 0, wx.ALL | wx.EXPAND, 5) left_sizer.Add(self.listbox1, 1, wx.ALL | wx.EXPAND, 5) # 比例為1,自動擴展 main_sizer.Add(left_sizer, 1, wx.EXPAND)
布局參數(shù)解析:
proportion(第二個參數(shù)):0:固定大小,不隨窗口縮放1:按比例分配剩余空間
flag(第三個參數(shù)):wx.ALL:四周都有邊距wx.EXPAND:填充可用空間
border(第四個參數(shù)):邊距像素值
3. 文件加載機制
def load_images(self):
self.image_files = []
self.listbox1.Clear()
self.image_modifications = {}
# 支持的圖片格式
image_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff')
try:
for filename in os.listdir(self.image_folder):
if filename.lower().endswith(image_extensions):
self.image_files.append(filename)
self.listbox1.Append(filename)
# 初始化每張圖片的編輯狀態(tài)
self.image_modifications[filename] = {'rotation': 0, 'crop': None}
技術(shù)要點:
- 文件過濾:使用
lower()和endswith()實現(xiàn)大小寫不敏感的文件擴展名匹配 - 狀態(tài)初始化:加載圖片時同時初始化編輯狀態(tài)字典
- 異常處理:使用try-except捕獲文件操作異常
4. 圖片預(yù)覽:多選ListBox的處理
這是本項目的一個重要技術(shù)點:
def on_select_image(self, event):
selections = self.listbox1.GetSelections() # 獲取所有選中項
if selections:
# 只預(yù)覽第一個選中的圖片
filename = self.listbox1.GetString(selections[0])
self.show_preview(filename)
為什么要用GetSelections()?
ListBox設(shè)置了wx.LB_EXTENDED樣式(多選模式):
self.listbox1 = wx.ListBox(panel, style=wx.LB_EXTENDED)
在多選模式下:
- ?
GetSelection():會拋出斷言錯誤 - ?
GetSelections():返回所有選中項的索引列表
這是wxPython的一個常見陷阱,必須根據(jù)ListBox的模式選擇正確的方法。
5. 圖片預(yù)覽實現(xiàn)
def show_preview(self, filename):
try:
image_path = os.path.join(self.image_folder, filename)
img = Image.open(image_path)
# 應(yīng)用已保存的編輯
if filename in self.image_modifications:
mods = self.image_modifications[filename]
# 應(yīng)用旋轉(zhuǎn)
if mods['rotation'] != 0:
img = img.rotate(-mods['rotation'], expand=True)
# 應(yīng)用剪切
if mods['crop']:
img = img.crop(mods['crop'])
self.current_preview_image = img.copy()
self.current_preview_filename = filename
# 縮放適應(yīng)預(yù)覽區(qū)域
preview_size = (450, 500)
img.thumbnail(preview_size, Image.Resampling.LANCZOS)
# PIL圖像轉(zhuǎn)wxImage
width, height = img.size
if img.mode != 'RGB':
img = img.convert('RGB') # 確保RGB模式
wx_image = wx.Image(width, height)
wx_image.SetData(img.tobytes())
# 顯示
self.image_preview.SetBitmap(wx_image.ConvertToBitmap())
self.image_preview.Refresh()
關(guān)鍵技術(shù)點:
PIL旋轉(zhuǎn):rotate(-angle, expand=True)
- 負角度:因為PIL的坐標系與界面旋轉(zhuǎn)方向相反
expand=True:自動擴展畫布以容納旋轉(zhuǎn)后的圖像
PIL剪切:crop((x1, y1, x2, y2))
- 坐標是左上角(x1, y1)和右下角(x2, y2)
圖像模式轉(zhuǎn)換:
if img.mode != 'RGB':
img = img.convert('RGB')
某些圖片格式(如PNG的RGBA、灰度圖)需要轉(zhuǎn)換為RGB才能在wxPython中顯示
PIL與wxPython的橋接:
wx_image = wx.Image(width, height) wx_image.SetData(img.tobytes()) # 字節(jié)數(shù)據(jù)傳遞 bitmap = wx_image.ConvertToBitmap()
6. 圖片旋轉(zhuǎn)功能
def on_rotate_left(self, event):
if self.current_preview_filename:
filename = self.current_preview_filename
# 模360運算保持角度在0-359范圍內(nèi)
self.image_modifications[filename]['rotation'] = \
(self.image_modifications[filename]['rotation'] - 90) % 360
self.show_preview(filename)
def on_rotate_right(self, event):
if self.current_preview_filename:
filename = self.current_preview_filename
self.image_modifications[filename]['rotation'] = \
(self.image_modifications[filename]['rotation'] + 90) % 360
self.show_preview(filename)
設(shè)計思路:
- 旋轉(zhuǎn)操作只修改數(shù)據(jù),不直接操作圖片文件
- 使用模運算
% 360確保角度值規(guī)范化 - 立即調(diào)用
show_preview()刷新顯示
7. 圖片剪切功能:自定義對話框
剪切功能的實現(xiàn)較為復(fù)雜,單獨使用了CropDialog類:
class CropDialog(wx.Dialog):
def __init__(self, parent, image):
super().__init__(parent, title="剪切圖片", size=(800, 700))
self.original_image = image.copy()
self.display_image = None
self.crop_box = None
self.start_point = None # 鼠標起始點
self.end_point = None # 鼠標結(jié)束點
self.scale_factor = 1.0 # 縮放比例
核心機制:
a) 圖像縮放與坐標映射
def prepare_image(self):
canvas_size = (750, 550)
img = self.original_image.copy()
img.thumbnail(canvas_size, Image.Resampling.LANCZOS)
self.display_image = img
# 計算縮放比例,用于坐標轉(zhuǎn)換
self.scale_factor = self.original_image.size[0] / img.size[0]
這個scale_factor非常關(guān)鍵:
- 顯示的是縮放后的圖像
- 用戶在縮放圖像上選擇區(qū)域
- 必須將坐標轉(zhuǎn)換回原圖尺寸
b) 鼠標事件處理
def on_mouse_down(self, event):
self.start_point = event.GetPosition()
self.end_point = self.start_point
def on_mouse_move(self, event):
if event.Dragging() and self.start_point:
self.end_point = event.GetPosition()
self.canvas.Refresh() # 觸發(fā)重繪
def on_mouse_up(self, event):
if self.start_point:
self.end_point = event.GetPosition()
self.canvas.Refresh()
事件流程:
- 按下鼠標 → 記錄起始點
- 拖動鼠標 → 更新結(jié)束點并刷新界面
- 釋放鼠標 → 確定最終選擇區(qū)域
c) 自定義繪制
def on_paint(self, event):
dc = wx.PaintDC(self.canvas)
if self.display_image:
# 繪制圖片
width, height = self.display_image.size
# ... 轉(zhuǎn)換并繪制圖片 ...
# 繪制選擇框
if self.start_point and self.end_point:
dc.SetPen(wx.Pen(wx.RED, 2))
dc.SetBrush(wx.Brush(wx.Colour(255, 0, 0, 50))) # 半透明紅色
x1, y1 = self.start_point
x2, y2 = self.end_point
rect_x = min(x1, x2)
rect_y = min(y1, y2)
rect_w = abs(x2 - x1)
rect_h = abs(y2 - y1)
dc.DrawRectangle(rect_x, rect_y, rect_w, rect_h)
DC(Device Context)繪圖:
wx.PaintDC:專門用于EVT_PAINT事件的繪圖上下文SetPen():設(shè)置線條樣式SetBrush():設(shè)置填充樣式DrawRectangle():繪制矩形
d) 坐標轉(zhuǎn)換
def get_crop_box(self):
if not self.start_point or not self.end_point:
return None
canvas_width, canvas_height = self.canvas.GetSize()
img_width, img_height = self.display_image.size
# 計算圖片在canvas中的偏移(居中顯示)
offset_x = (canvas_width - img_width) // 2
offset_y = (canvas_height - img_height) // 2
# 轉(zhuǎn)換為圖片坐標
x1 = max(0, min(self.start_point[0], self.end_point[0]) - offset_x)
y1 = max(0, min(self.start_point[1], self.end_point[1]) - offset_y)
x2 = min(img_width, max(self.start_point[0], self.end_point[0]) - offset_x)
y2 = min(img_height, max(self.start_point[1], self.end_point[1]) - offset_y)
# 轉(zhuǎn)換回原始圖片坐標(考慮縮放)
orig_x1 = int(x1 * self.scale_factor)
orig_y1 = int(y1 * self.scale_factor)
orig_x2 = int(x2 * self.scale_factor)
orig_y2 = int(y2 * self.scale_factor)
# 驗證選擇區(qū)域大小
if orig_x2 - orig_x1 > 10 and orig_y2 - orig_y1 > 10:
return (orig_x1, orig_y1, orig_x2, orig_y2)
return None
坐標轉(zhuǎn)換步驟:
- 減去偏移量:canvas坐標 → 顯示圖片坐標
- 邊界檢查:確保坐標在圖片范圍內(nèi)
- 縮放轉(zhuǎn)換:顯示圖片坐標 → 原圖坐標
- 大小驗證:確保選擇區(qū)域不是太小
8. 列表管理功能
def on_add_to_list2(self, event):
selections = self.listbox1.GetSelections()
for sel in selections:
filename = self.listbox1.GetString(sel)
# 防止重復(fù)添加
if self.listbox2.FindString(filename) == wx.NOT_FOUND:
self.listbox2.Append(filename)
def on_remove_from_list2(self, event):
selections = list(self.listbox2.GetSelections())
selections.reverse() # 從后往前刪除,避免索引變化問題
for sel in selections:
self.listbox2.Delete(sel)
刪除技巧:
為什么要反向刪除?
# 假設(shè)選中索引 [1, 3, 5] selections = [1, 3, 5] selections.reverse() # 變成 [5, 3, 1] # 正向刪除的問題: Delete(1) # 刪除索引1,后面的元素前移 # 現(xiàn)在原來的索引3變成了索引2,但我們要刪除索引3 ? # 反向刪除: Delete(5) # 刪除索引5,不影響前面的元素 Delete(3) # 刪除索引3,不影響前面的元素 Delete(1) # 刪除索引1 ?
9. 順序調(diào)整
def on_move_up(self, event):
selections = self.listbox2.GetSelections()
if selections and selections[0] > 0:
selection = selections[0]
filename = self.listbox2.GetString(selection)
self.listbox2.Delete(selection)
self.listbox2.Insert(filename, selection - 1)
self.listbox2.SetSelection(selection - 1) # 保持選中狀態(tài)
實現(xiàn)邏輯:
- 獲取當前選中項
- 刪除該項
- 在新位置插入
- 重新選中(提供更好的用戶體驗)
10. PDF生成:核心功能
def create_pdf(self, pdf_path):
try:
c = canvas.Canvas(pdf_path, pagesize=A4)
page_width, page_height = A4
for i in range(self.listbox2.GetCount()):
filename = self.listbox2.GetString(i)
image_path = os.path.join(self.image_folder, filename)
# 加載并應(yīng)用編輯
img = Image.open(image_path)
if filename in self.image_modifications:
mods = self.image_modifications[filename]
if mods['rotation'] != 0:
img = img.rotate(-mods['rotation'], expand=True)
if mods['crop']:
img = img.crop(mods['crop'])
# 轉(zhuǎn)換為RGB模式
if img.mode != 'RGB':
img = img.convert('RGB')
# 保存到內(nèi)存緩沖區(qū)
img_buffer = io.BytesIO()
img.save(img_buffer, format='JPEG')
img_buffer.seek(0)
img_width, img_height = img.size
# 計算縮放比例,適應(yīng)A4頁面
width_ratio = (page_width - 40) / img_width
height_ratio = (page_height - 40) / img_height
ratio = min(width_ratio, height_ratio) # 保持寬高比
new_width = img_width * ratio
new_height = img_height * ratio
# 居中顯示
x = (page_width - new_width) / 2
y = (page_height - new_height) / 2
# 添加到PDF
c.drawImage(ImageReader(img_buffer), x, y,
width=new_width, height=new_height)
# 添加新頁(除最后一頁)
if i < self.listbox2.GetCount() - 1:
c.showPage()
c.save()
技術(shù)亮點:
a) 內(nèi)存緩沖區(qū)
img_buffer = io.BytesIO() img.save(img_buffer, format='JPEG') img_buffer.seek(0)
為什么使用內(nèi)存緩沖區(qū)?
- ? 不需要創(chuàng)建臨時文件
- ? 提高性能
- ? 避免磁盤IO開銷
b) 寬高比保持
width_ratio = (page_width - 40) / img_width height_ratio = (page_height - 40) / img_height ratio = min(width_ratio, height_ratio) # 選擇較小的比例
使用min()確保圖片完整顯示在頁面內(nèi),不會被裁剪。
c) 居中對齊
x = (page_width - new_width) / 2 y = (page_height - new_height) / 2
通過計算剩余空間的一半,實現(xiàn)居中效果。
優(yōu)化建議與擴展思路
1. 性能優(yōu)化
問題:大量圖片加載時可能卡頓
解決方案:
# 使用線程加載圖片
import threading
def load_images_async(self):
def load():
# 加載邏輯
wx.CallAfter(self.update_ui) # 線程安全的UI更新
thread = threading.Thread(target=load)
thread.start()
2. 縮略圖緩存
優(yōu)化:
self.thumbnail_cache = {} # 添加緩存字典
def get_thumbnail(self, filename):
if filename not in self.thumbnail_cache:
img = Image.open(...)
img.thumbnail((100, 100))
self.thumbnail_cache[filename] = img
return self.thumbnail_cache[filename]
3. 批量操作
建議:添加批量旋轉(zhuǎn)、批量剪切功能
def on_batch_rotate(self, event):
selections = self.listbox2.GetSelections()
for sel in selections:
filename = self.listbox2.GetString(sel)
self.image_modifications[filename]['rotation'] += 90
4. 撤銷/重做功能
實現(xiàn)思路:
class CommandHistory:
def __init__(self):
self.history = []
self.current = -1
def add_command(self, command):
self.history = self.history[:self.current+1]
self.history.append(command)
self.current += 1
def undo(self):
if self.current >= 0:
self.history[self.current].undo()
self.current -= 1
5. 配置保存
建議:保存用戶的編輯狀態(tài)
import json
def save_project(self):
project = {
'folder': self.image_folder,
'modifications': self.image_modifications,
'export_list': [self.listbox2.GetString(i)
for i in range(self.listbox2.GetCount())]
}
with open('project.json', 'w') as f:
json.dump(project, f)
常見問題與解決
Q1: 為什么圖片顯示不出來?
可能原因:
- 圖片模式不是RGB
- 文件路徑編碼問題
- 圖片損壞
解決:
# 始終轉(zhuǎn)換為RGB
if img.mode != 'RGB':
img = img.convert('RGB')
# 處理路徑編碼
image_path = os.path.join(self.image_folder, filename)
# 在Windows上可能需要:
# image_path = image_path.encode('utf-8').decode('utf-8')
Q2: PDF生成后圖片質(zhì)量下降?
原因:使用JPEG格式壓縮
解決:
# 提高JPEG質(zhì)量 img.save(img_buffer, format='JPEG', quality=95) # 或使用PNG(文件更大) img.save(img_buffer, format='PNG')
Q3: 大圖片加載很慢?
解決:使用漸進式加載
# 先顯示低分辨率版本 thumbnail = img.copy() thumbnail.thumbnail((200, 200)) # 顯示縮略圖... # 異步加載完整版 threading.Thread(target=lambda: self.load_full_image(img)).start()
運行結(jié)果

以上就是Python基于wxPython開發(fā)一個圖片PDF生成器的詳細內(nèi)容,更多關(guān)于Python wxPython圖片PDF生成器的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用Pycharm在運行過程中,查看每個變量的操作(show variables)
這篇文章主要介紹了使用Pycharm在運行過程中,查看每個變量的操作(show variables),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-06-06
基于python requests selenium爬取excel vba過程解析
這篇文章主要介紹了基于python requests selenium爬取excel vba過程解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-08-08
解決python3報錯之takes?1?positional?argument?but?2?were?gi
這篇文章主要介紹了解決python3報錯之takes?1?positional?argument?but?2?were?given問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03
tensorflow 固定部分參數(shù)訓(xùn)練,只訓(xùn)練部分參數(shù)的實例
今天小編就為大家分享一篇tensorflow 固定部分參數(shù)訓(xùn)練,只訓(xùn)練部分參數(shù)的實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-01-01
PyInstaller將Python項目打包為exe的踩坑記錄
在python開發(fā)中,將一個能順暢運行的項目打包成獨立的可執(zhí)行文件是交付給用戶的關(guān)鍵一步,PyInstaller 無疑是這個領(lǐng)域的王者,下面小編就來和大家詳細介紹一下吧2025-08-08
PyTorch常用函數(shù)torch.cat()中dim參數(shù)使用說明
這篇文章主要為大家介紹了PyTorch常用函數(shù)torch.cat()中dim參數(shù)使用說明,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-04-04

