一、开发背景与适用场景

随着数字文档处理需求的激增,图片转PDF的需求日益广泛。从学生提交图像化作业,到教师整合扫描试卷等资料,再到行政人员归档证件照片,工作中的方方面面都离不开图片的处理。如何高效、批量地将多个图片文件整理为一个标准PDF文件,成为日常办公中的痛点之一。

尽管市面上已有诸多工具支持图像转PDF,但大多数要么受限于平台(如仅支持Windows或Web)、要么操作繁琐、功能单一。而借助Python强大的图像处理与GUI库,我们可以快速构建一款轻量级、跨平台、功能丰富的图片转PDF工具,适用于:

  • 教育场景下学生作业或试卷扫描;
  • 办公场景下的证件照片批量归档;
  • 图片编辑后统一格式输出;
  • 快速制作图文资料手册等。

软件的主界面

二、主要功能与技术特点

本项目以 Tkinter 为GUI主框架,集成 PIL (Pillow) 处理图像、threading 异步转PDF、shutil 文件管理等模块,具备如下功能:

1. 多图管理与预览

  • 支持批量添加、删除、清空图片;
  • 支持上下移动图片列表中的文件排序,决定PDF页序;
  • 实时图片预览,并在右侧展示缩略图;

2. 图片编辑操作

  • 支持图片的 90° 左右旋转;
  • 支持水平 / 垂直翻转;
  • 支持单张图片的“重置”功能,回到原始状态;
  • 所有图像操作均在不影响原图基础上完成,保留数据完整性;

3. 文件命名与批处理

  • 用户可输入模板(如“Page_##.jpg”)对所有图片批量重命名; 比如你们把Page改为作业_##.jpg,生成的图片就变成作业_01.jpg, 作业_02.jpg,作业_03.jpg
  • 自动创建名为 renamed_images/ 的目录并保存副本,确保原图不被覆盖;

图片重命名

4. PDF转换与保存

  • 一键将所有图片按设定顺序合成并转化为PDF文件;
  • 异步转换不阻塞界面响应,转化速度高;
  • 支持软件底部进度条反馈转换进度;
  • 自动处理图像色彩模式为PDF兼容的RGB格式;

5. 人性化交互设计

  • 键盘快捷操作支持:向上 / 向下键调整顺序,Delete键删除选中;
  • 弹窗提示操作状态,如保存路径、错误提示、任务完成反馈等。

三、使用方法与操作流程

1. 启动程序

运行脚本后,程序将自动启动一个800×600的窗口,主界面分为顶部按钮栏、左侧列表框和右侧预览区。

2. 添加与管理图片

点击“添加图片”可选取多张图片文件导入,随后可在列表中拖动排序,或使用“上移 / 下移”按钮调整顺序。

3. 图像编辑与重命名

选中某张图片后,可以在右侧操作区执行旋转、翻转等编辑操作。如需统一重命名图片,点击“批量重命名”,输入模板后自动保存副本到 renamed_images/ 文件夹中。

4. 转换与保存PDF

点击“转换为PDF”,选择保存位置后,程序将自动执行图像转换、拼接与导出过程。用户可实时查看进度条反馈。

四、代码展示

本项目采用类的写法,主要运用了Python的自带的标准库如os,shutil等,完整的代码如下:

import os
import shutil
import threading
import tkinter as tk
from tkinter import filedialog, messagebox, simpledialog, ttk
from PIL import Image, ImageTkclass ImageToPDFApp:def __init__(self, root):self.root = rootself.root.title("图片转PDF工具")self.root.geometry("800x600")self.image_paths = []self.modified_images = {}self.last_dir = os.path.expanduser("~")self.renamed_dir = os.path.join(os.getcwd(), "renamed_images")self.build_ui()def build_ui(self):top_frame = tk.Frame(self.root, bg="#f0f0f0")top_frame.pack(fill=tk.X, padx=5, pady=5)buttons = [("添加图片", self.add_images),("删除选中", self.remove_selected),("上移", self.move_up),("下移", self.move_down),("清空列表", self.clear_list),("转换为PDF", self.convert_to_pdf)]for text, cmd in buttons:b = tk.Button(top_frame, text=text, command=cmd, width=12, font=("Times New Roman", 12))b.pack(side=tk.LEFT, padx=3, pady=2)middle_frame = tk.Frame(self.root)middle_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)left_frame = tk.Frame(middle_frame)left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)self.lst_images = tk.Listbox(left_frame)self.lst_images.pack(fill=tk.BOTH, expand=True)self.lst_images.bind("<<ListboxSelect>>", self.update_preview)#增加快捷键self.lst_images.bind("<Up>", self.on_key_up)self.lst_images.bind("<Down>", self.on_key_down)self.lst_images.bind("<Delete>", self.on_key_delete)preview_frame = tk.Frame(middle_frame)preview_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)self.preview_label = tk.Label(preview_frame, text="图片预览", relief=tk.SUNKEN, bg="white")self.preview_label.pack(side=tk.LEFT, expand=True, fill=tk.BOTH, padx=5)ctrl_frame = tk.Frame(preview_frame)ctrl_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=5)ctrl_buttons = [("向左旋转 (90°)", lambda: self.transform_image('rotate_left')),("向右旋转 (90°)", lambda: self.transform_image('rotate_right')),("水平翻转", lambda: self.transform_image('flip_h')),("垂直翻转", lambda: self.transform_image('flip_v')),("重置", self.reset_image),("批量重命名", self.batch_rename_images)  # 添加按钮]for text, cmd in ctrl_buttons:btn = tk.Button(ctrl_frame, text=text, command=cmd, width=14, font=("Times New Roman", 12))btn.pack(fill=tk.X, pady=3)self.img_count_label = tk.Label(ctrl_frame, text="已添加 0 张图片", font=("Times New Roman", 12), anchor='w')self.img_count_label.pack(side=tk.BOTTOM, pady=5, fill=tk.X)self.progress = ttk.Progressbar(self.root, mode="determinate")self.progress.pack(fill=tk.X, padx=5, pady=5)def on_key_up(self, event):self.move_up()def on_key_down(self, event):self.move_down()def on_key_delete(self, event):self.remove_selected()def add_images(self):files = filedialog.askopenfilenames(filetypes=[("Image files", "*.png *.jpg *.jpeg *.bmp *.gif *.tiff")],initialdir=self.last_dir)if files:for f in files:if f not in self.image_paths:self.image_paths.append(f)self.lst_images.insert(tk.END, os.path.basename(f))self.img_count_label.config(text=f"已添加 {len(self.image_paths)} 张图片")self.lst_images.select_set(0)self.update_preview()def remove_selected(self):selected = self.lst_images.curselection()for i in reversed(selected):del self.image_paths[i]self.lst_images.delete(i)self.img_count_label.config(text=f"已添加 {len(self.image_paths)} 张图片")self.preview_label.config(image='', text='图片预览')def move_up(self):i = self.lst_images.curselection()if i and i[0] > 0:idx = i[0]self.image_paths[idx - 1], self.image_paths[idx] = self.image_paths[idx], self.image_paths[idx - 1]self.lst_images.delete(idx)self.lst_images.insert(idx - 1, os.path.basename(self.image_paths[idx - 1]))self.lst_images.select_set(idx - 1)self.update_preview()def move_down(self):i = self.lst_images.curselection()if i and i[0] < len(self.image_paths) - 1:idx = i[0]self.image_paths[idx + 1], self.image_paths[idx] = self.image_paths[idx], self.image_paths[idx + 1]self.lst_images.delete(idx)self.lst_images.insert(idx + 1, os.path.basename(self.image_paths[idx + 1]))self.lst_images.select_set(idx + 1)self.update_preview()def clear_list(self):self.image_paths.clear()self.lst_images.delete(0, tk.END)self.modified_images.clear()self.preview_label.config(image='', text='图片预览')self.img_count_label.config(text="已添加 0 张图片")def update_preview(self, event=None):if not self.lst_images.curselection():returnidx = self.lst_images.curselection()[0]path = self.image_paths[idx]img = self.modified_images.get(path, Image.open(path))img.thumbnail((300, 300))self.tkimg = ImageTk.PhotoImage(img)self.preview_label.config(image=self.tkimg, text='')def transform_image(self, action):idxs = self.lst_images.curselection()if not idxs:returnidx = idxs[0]path = self.image_paths[idx]img = self.modified_images.get(path, Image.open(path))if action == 'rotate_left':img = img.rotate(90, expand=True)elif action == 'rotate_right':img = img.rotate(-90, expand=True)elif action == 'flip_h':img = img.transpose(Image.FLIP_LEFT_RIGHT)elif action == 'flip_v':img = img.transpose(Image.FLIP_TOP_BOTTOM)self.modified_images[path] = imgself.update_preview()def reset_image(self):idx = self.lst_images.curselection()if not idx:returnpath = self.image_paths[idx[0]]self.modified_images.pop(path, None)self.update_preview()def batch_rename_images(self):if not self.image_paths:messagebox.showwarning("警告", "没有图片可重命名")returntemplate = simpledialog.askstring("命名模板", "请输入命名模板(如 Page_##.jpg):", initialvalue="Page_##.jpg")if not template or "##" not in template:messagebox.showwarning("格式错误", "命名模板必须包含 ## 作为编号占位符")returnif os.path.exists(self.renamed_dir):shutil.rmtree(self.renamed_dir)os.makedirs(self.renamed_dir, exist_ok=True)new_paths = []for i, path in enumerate(self.image_paths):new_name = template.replace("##", f"{i+1:02}")new_path = os.path.join(self.renamed_dir, new_name)shutil.copy2(path, new_path)new_paths.append(new_path)self.image_paths = new_pathsself.lst_images.delete(0, tk.END)for path in self.image_paths:self.lst_images.insert(tk.END, os.path.basename(path))self.lst_images.select_set(0)self.update_preview()messagebox.showinfo("完成", f"已重命名图片,保存在: {self.renamed_dir}")def convert_to_pdf(self):if not self.image_paths:messagebox.showwarning("警告", "没有图片可转换")returnoutput_path = filedialog.asksaveasfilename(defaultextension=".pdf", filetypes=[("PDF文件", "*.pdf")])if not output_path:returndef task():images = []for i, path in enumerate(self.image_paths):img = self.modified_images.get(path, Image.open(path))if img.mode != 'RGB':img = img.convert('RGB')images.append(img)self.progress['value'] = int((i + 1) / len(self.image_paths) * 100)self.root.update_idletasks()if images:images[0].save(output_path, save_all=True, append_images=images[1:])messagebox.showinfo("成功", f"PDF已保存至: {output_path}")self.progress['value'] = 0threading.Thread(target=task).start()
if __name__ == '__main__':root = tk.Tk()app = ImageToPDFApp(root)root.mainloop()

五、Python开发项目的优势

采用Python开发出的软件具有多重优势如下:

跨平台:Python支持Windows、macOS、Linux,代码无需修改即可兼容多系统,同时免费;

丰富的生态:Pillow、Tkinter、shutil 等库直接支持图像处理、GUI与文件操作,无需自行封装底层逻辑;

快速迭代:Python代码结构清晰,便于快速开发与后期维护,可以进行个性化扩展,满足多种多样的需求;

大语言模型友好:大语言模型可以充分网上丰富的资源,对于软件的设计、开发给出建议,辅助生成功能代码。

六、学后总结

1. 在ChatGPT的辅助下编写一个简洁高效的个性化图片转PDF工具,不仅解决了实际办公与学习场景中的具体问题,也展示了Python在图形界面应用、批处理和跨平台工具开发中的巨大潜力。

2. 此工具可能根据个人实际的需要,扩展添加如OCR识别、水印添加、图片压缩、图片放大等模块,使其逐步成长为一款专业的文档图像工具箱。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/910183.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/910183.shtml
英文地址,请注明出处:http://en.pswp.cn/news/910183.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SuGAR代码精简解读

目录 一、全流程训练脚本 train_full_pipeline.py 二、核心训练逻辑 train.py 粗优化 (coarse_density_and_dn_consistency.py) 网格提取 (extract_mesh_from_coarse_sugar) 精优化 (refined_training) 两次优化&#xff08;粗优化和精优化&#xff09;中使用的损失函数及…

大模型安全关键技术研究

​ 引言 随着人工智能技术的迅猛发展&#xff0c;大模型已成为推动各行业变革的核心力量。从智能客服、医疗影像识别到金融风险预测&#xff0c;大模型的应用场景不断拓展&#xff0c;深刻改变着人们的生产生活方式。大模型已经转变为AI领域的基础设施&#xff0c;为解决各种…

java面试题04成员变量和局部变量的区别

成员变量(Member Variable)和局部变量(Local Variable)是面向对象编程中两种作用域和生命周期不同的变量,主要区别体现在以下几个方面: 1. 声明位置 成员变量: 声明在类内部、方法/构造器/代码块外部。 例如: public class Person {// 成员变量(实例变量)private Str…

升级到 .NET 9 分步指南

随着激动人心的 .Net 9 更新正式发布&#xff0c;漫长的等待终于结束了。它带来了一些令人惊叹的特性&#xff0c;例如改进的 LINQ 功能、HybridCache 等等。此外&#xff0c;凭借其卓越的性能提升、更佳的安全性、更完善的协议和更易维护的特性&#xff0c;它必将吸引开发者和…

day30打卡

# 导入模块 import math print("方式1&#xff1a;使用 import math") print(f"圆周率π的值&#xff1a;{math.pi}") print(f"2的平方根&#xff1a;{math.sqrt(2)}\n") # 导入特定项 from math import pi, sqrt print("方式2&#…

优化数据库查询

优化数据库查询 在实际开发中,数据库查询的性能直接关系到系统响应速度和用户体验。尤其在高并发环境下,低效的SQL语句会成为瓶颈,导致系统负载升高,甚至引发宕机。因此,查询优化是数据库性能优化中最为关键的一环。 为了系统性地理解数据库查询优化策略,本节将从SQL语…

【LeetCode#第198题】打家劫舍(一维dp)

198. 打家劫舍 - 力扣&#xff08;LeetCode&#xff09; 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被小偷闯入&#…

微前端MFE:(React 与 Angular)框架之间的通信方式

在 微前端&#xff08;MFE, Micro Frontends&#xff09; 中使用 CustomEvent 是非常常见的&#xff0c;尤其是在不同子应用&#xff08;Micro Apps&#xff09;之间通信的时候。今天将以React 主应用 ↔ Angular 子应用 之间的通信进行示例 React 主应用 <-> Angular 子…

408考研逐题详解:2010年第1题——理解栈的基本操作

2010年第1题 若元素 a&#xff0c;b&#xff0c;c&#xff0c;d&#xff0c;e&#xff0c;f 依次进栈&#xff0c;允许进栈、退栈操作交替进行&#xff0c;但不允许连续三次进行退栈操作&#xff0c;则不可能得到的出栈序列是&#xff08; &#xff09; A. dcebfa \qquad B.…

python追加合并excel效率记录

第一种合并方法&#xff1a; 在sheet的第一行&#xff0c;追加新表concat旧表 read_excel读取旧表全部 to_excel新表追加写入旧表 需要的时间&#xff1a; 第二种合并方法&#xff1a; 在sheet的最后一行&#xff0c;直接追加新表 load_book只读用来获取旧表sheet行数 read_ex…

公钥加密与签名算法计算详解(含计算题例子)

一、RSA 加密算法 密钥生成&#xff1a; 选两个大素数 p 和 q计算 n p q计算 φ(n) (p-1)(q-1)选整数 e 满足 1 < e < φ(n) 且 gcd(e, φ(n)) 1计算 d 满足 d e ≡ 1 mod φ(n) 公钥&#xff1a;(e, n) 私钥&#xff1a;(d, n) 加密&#xff1a; c ≡ mᵉ mod…

63 网络交互的过程中目标设备的选择

前言 这里主要是 调研一下 发送网络数据包的过程中 选择网络设备 比如 向本机发送信息, 走的是 lo 向局域网其他主机发送信息, 走无线网卡 或者 有线网卡 基于 linux 的调试 这里主要是基于 ping 192.168.1.2 的调试 skb->dev 的初始化是在 skb->_skb_refdst 初…

DE2-115板子上用 Verilog编程实现一个分秒计数器

一、实验目的 掌握 Verilog 语言在硬件描述中的应用&#xff0c;通过编程实现分秒计数器的逻辑功能。 学习并实践按键消抖的原理与实现方法&#xff0c;提升对硬件电路中信号处理的理解。 熟悉在 DE2-115 开发板上进行 Verilog 程序的开发、调试及下载验证流程&#xff0c;将…

R4 LSTM-火灾温度预测

import tensorflow as tf import pandas as pd import numpy as npgpus tf.config.list_physical_devices("GPU") if gpus:tf.config.experimental.set_memory_growth(gpus[0], True) #设置GPU显存用量按需使用tf.config.set_visible_devices([gpus[0]],&…

什么是跨域问题?后端如何解决跨域问题?

跨域问题是指浏览器为了安全&#xff0c;对不同域&#xff08;包含不同协议、不同端口或不同主机名&#xff09;的请求进行限制&#xff0c;从而导致请求无法正常访问后端接口。 跨域问题的产生源于浏览器的同源策略&#xff08;Same-Origin Policy&#xff09;&#xff0c;这…

vue | rollup 打包 | 配置 rollup.config.js 文件,更改 rollup的行为

原因&#xff1a;将入口文件 转为 esm 和 umd 两种格式&#xff0c;要配置 rollup Rollup 已内置到 vite 工具中&#xff0c; 命令行打包&#xff0c;参数多&#xff0c;麻烦——》解决&#xff1a;创建配置文件&#xff0c;js 写的&#xff0c;rollup.config.js 配置 rollup.…

服务器中物理处理器和逻辑处理器的区别?

在服务器或任何计算机系统中&#xff0c;**物理处理器&#xff08;Physical Processor&#xff09;和逻辑处理器&#xff08;Logical Processor&#xff09;**是两个不同的概念&#xff0c;它们分别代表了硬件层面和操作系统层面的处理能力。 物理处理器&#xff08;Physical P…

【Gin框架】中间件

1. 什么是中间件 (Middleware)&#xff1f; 在 Web 框架的语境下&#xff0c;中间件 (Middleware) 是一种可重用的软件组件或函数&#xff0c;它被设计用来在 HTTP 请求-响应生命周期中的特定点拦截和处理请求或响应。在 Gin 框架中&#xff0c;中间件特指符合 gin.HandlerFun…

STUN (Session Traversal Utilities for NAT) 服务器是一种网络协议

STUN (Session Traversal Utilities for NAT) 服务器是一种网络协议&#xff0c;主要用于帮助位于网络地址转换 (NAT) 设备&#xff08;如路由器&#xff09;后面的客户端发现自己的公共 IP 地址和端口号。这对于建立点对点 (P2P) 通信至关重要&#xff0c;尤其是在 VoIP&#…

AQS详解

概念 AQS&#xff08;AbstractQueuedSynchronizer&#xff09; 是并发包&#xff08;java.util.concurrent&#xff09;的核心组件&#xff0c;用于构建锁和同步器&#xff08;如 ReentrantLock、Semaphore、CountDownLatch 等&#xff09;。它通过维护一个 CLH 队列 和 同步状…