python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python wxPython图片PDF生成器

Python基于wxPython开发一个图片PDF生成器

作者:winfredzhang

本文将详细解析一个基于wxPython开发的图片PDF生成器应用程序,该程序能够批量处理图片,支持旋转、剪切等编辑功能,并按照指定顺序将图片导出为PDF文件,需要的朋友可以参考下

前言

本文将详细解析一个基于wxPython开发的图片PDF生成器应用程序。该程序能够批量处理图片,支持旋转、剪切等编辑功能,并按照指定顺序将图片导出为PDF文件。通过本文,你将学习到wxPython GUI开发、PIL图像处理、ReportLab PDF生成等技术要点。
C:\pythoncode\new\pics2pdf.py

项目概述

功能特性

  1. 图片管理:浏览文件夹并加载所有图片文件
  2. 图片预览:实时预览选中的图片
  3. 图片编辑
    • 旋转功能(支持90度左旋/右旋)
    • 剪切功能(鼠标拖拽选择区域)
    • 重置功能(恢复原始状态)
  4. 列表管理:在两个列表间移动图片,调整顺序
  5. PDF导出:按指定顺序将图片导出为PDF文件

技术栈

代码架构分析

整体结构

程序采用面向对象设计,主要包含两个类:

  1. ImagePDFFrame:主窗口类,负责整体UI和核心功能
  2. CropDialog:剪切对话框类,负责图片剪切功能
ImagePDFFrame (主窗口)
├── UI布局
│   ├── 左侧:图片列表 + 选择文件夹按钮
│   ├── 中间:添加/移除按钮
│   ├── 右侧:待导出列表 + 顺序调整按钮
│   └── 预览区:图片显示 + 编辑按钮
├── 数据管理
│   ├── image_folder:当前文件夹路径
│   ├── image_files:图片文件列表
│   ├── image_modifications:图片编辑记录
│   └── current_preview_image:当前预览图片
└── 功能模块
    ├── 文件加载
    ├── 图片预览
    ├── 图片编辑
    └── 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 = {}  # 关键数据结构:存储图片编辑信息

数据结构设计亮点

image_modifications 字典的设计非常巧妙:

{
    'photo1.jpg': {
        'rotation': 90,      # 旋转角度(0, 90, 180, 270)
        'crop': (x1, y1, x2, y2)  # 剪切区域坐标
    },
    'photo2.jpg': {
        'rotation': 0,
        'crop': None
    }
}

这种设计的优势:

2. UI布局:BoxSizer的应用

wxPython使用Sizer进行布局管理,代码中使用了wx.BoxSizer

main_sizer = wx.BoxSizer(wx.HORIZONTAL)  # 水平主布局

# 左侧垂直布局
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)

布局参数解析

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)
                # 初始化每张图片的编辑状态
                self.image_modifications[filename] = {'rotation': 0, 'crop': None}

技术要点

  1. 文件过滤:使用lower()endswith()实现大小写不敏感的文件扩展名匹配
  2. 状态初始化:加载图片时同时初始化编辑状态字典
  3. 异常处理:使用try-except捕获文件操作异常

4. 图片预览:多选ListBox的处理

这是本项目的一个重要技术点:

def on_select_image(self, event):
    selections = self.listbox1.GetSelections()  # 获取所有选中项
    if selections:
        # 只预览第一个选中的图片
        filename = self.listbox1.GetString(selections[0])
        self.show_preview(filename)

为什么要用GetSelections()?

ListBox设置了wx.LB_EXTENDED样式(多选模式):

self.listbox1 = wx.ListBox(panel, style=wx.LB_EXTENDED)

在多选模式下:

这是wxPython的一个常见陷阱,必须根据ListBox的模式选择正确的方法。

5. 图片预览实现

def show_preview(self, filename):
    try:
        image_path = os.path.join(self.image_folder, filename)
        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'])
        
        self.current_preview_image = img.copy()
        self.current_preview_filename = filename
        
        # 缩放适应预览区域
        preview_size = (450, 500)
        img.thumbnail(preview_size, Image.Resampling.LANCZOS)
        
        # PIL图像转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()

关键技术点

PIL旋转rotate(-angle, expand=True)

PIL剪切crop((x1, y1, x2, y2))

图像模式转换

if img.mode != 'RGB':
    img = img.convert('RGB')

某些图片格式(如PNG的RGBA、灰度图)需要转换为RGB才能在wxPython中显示

PIL与wxPython的桥接

wx_image = wx.Image(width, height)
wx_image.SetData(img.tobytes())  # 字节数据传递
bitmap = wx_image.ConvertToBitmap()

6. 图片旋转功能

def on_rotate_left(self, event):
    if self.current_preview_filename:
        filename = self.current_preview_filename
        # 模360运算保持角度在0-359范围内
        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)

设计思路

7. 图片剪切功能:自定义对话框

剪切功能的实现较为复杂,单独使用了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    # 鼠标结束点
        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
    # 计算缩放比例,用于坐标转换
    self.scale_factor = self.original_image.size[0] / img.size[0]

这个scale_factor非常关键:

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()  # 触发重绘

def on_mouse_up(self, event):
    if self.start_point:
        self.end_point = event.GetPosition()
        self.canvas.Refresh()

事件流程

  1. 按下鼠标 → 记录起始点
  2. 拖动鼠标 → 更新结束点并刷新界面
  3. 释放鼠标 → 确定最终选择区域

c) 自定义绘制

def on_paint(self, event):
    dc = wx.PaintDC(self.canvas)
    
    if self.display_image:
        # 绘制图片
        width, height = self.display_image.size
        # ... 转换并绘制图片 ...
        
        # 绘制选择框
        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)绘图

d) 坐标转换

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
    
    # 转换为图片坐标
    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)
    
    # 转换回原始图片坐标(考虑缩放)
    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)
    
    # 验证选择区域大小
    if orig_x2 - orig_x1 > 10 and orig_y2 - orig_y1 > 10:
        return (orig_x1, orig_y1, orig_x2, orig_y2)
    
    return None

坐标转换步骤

  1. 减去偏移量:canvas坐标 → 显示图片坐标
  2. 边界检查:确保坐标在图片范围内
  3. 缩放转换:显示图片坐标 → 原图坐标
  4. 大小验证:确保选择区域不是太小

8. 列表管理功能

def on_add_to_list2(self, event):
    selections = self.listbox1.GetSelections()
    for sel in selections:
        filename = self.listbox1.GetString(sel)
        # 防止重复添加
        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)

删除技巧

为什么要反向删除?

# 假设选中索引 [1, 3, 5]
selections = [1, 3, 5]
selections.reverse()  # 变成 [5, 3, 1]

# 正向删除的问题:
Delete(1)  # 删除索引1,后面的元素前移
# 现在原来的索引3变成了索引2,但我们要删除索引3 ❌

# 反向删除:
Delete(5)  # 删除索引5,不影响前面的元素
Delete(3)  # 删除索引3,不影响前面的元素
Delete(1)  # 删除索引1 ✅

9. 顺序调整

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)  # 保持选中状态

实现逻辑

  1. 获取当前选中项
  2. 删除该项
  3. 在新位置插入
  4. 重新选中(提供更好的用户体验)

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)
            
            # 加载并应用编辑
            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'])
            
            # 转换为RGB模式
            if img.mode != 'RGB':
                img = img.convert('RGB')
            
            # 保存到内存缓冲区
            img_buffer = io.BytesIO()
            img.save(img_buffer, format='JPEG')
            img_buffer.seek(0)
            
            img_width, img_height = img.size
            
            # 计算缩放比例,适应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()

技术亮点

a) 内存缓冲区

img_buffer = io.BytesIO()
img.save(img_buffer, format='JPEG')
img_buffer.seek(0)

为什么使用内存缓冲区?

b) 宽高比保持

width_ratio = (page_width - 40) / img_width
height_ratio = (page_height - 40) / img_height
ratio = min(width_ratio, height_ratio)  # 选择较小的比例

使用min()确保图片完整显示在页面内,不会被裁剪。

c) 居中对齐

x = (page_width - new_width) / 2
y = (page_height - new_height) / 2

通过计算剩余空间的一半,实现居中效果。

优化建议与扩展思路

1. 性能优化

问题:大量图片加载时可能卡顿

解决方案

# 使用线程加载图片
import threading

def load_images_async(self):
    def load():
        # 加载逻辑
        wx.CallAfter(self.update_ui)  # 线程安全的UI更新
    
    thread = threading.Thread(target=load)
    thread.start()

2. 缩略图缓存

优化

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. 批量操作

建议:添加批量旋转、批量剪切功能

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. 撤销/重做功能

实现思路

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. 配置保存

建议:保存用户的编辑状态

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: 为什么图片显示不出来?

可能原因

  1. 图片模式不是RGB
  2. 文件路径编码问题
  3. 图片损坏

解决

# 始终转换为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生成后图片质量下降?

原因:使用JPEG格式压缩

解决

# 提高JPEG质量
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()

运行结果

以上就是Python基于wxPython开发一个图片PDF生成器的详细内容,更多关于Python wxPython图片PDF生成器的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文