PDF与图片互转WEB应用开发教程
目录
项目概述
功能需求
本项目是一个基于 Streamlit 框架的 Web 应用程序,实现 PDF 文件与图片文件之间的相互转换功能。
| 功能模块 | 描述 |
|---|---|
| 文件上传模块 | 支持用户上传 PDF 文件和常见格式的图片文件(JPG、PNG、BMP、GIF、TIFF) |
| PDF转图片功能 | 将上传的 PDF 文件逐页转换为图片,支持选择输出图片格式和质量设置 |
| 图片转PDF功能 | 将多张上传的图片按顺序合并转换为单个 PDF 文件,支持调整图片排序和页面方向设置 |
| 文件下载模块 | 提供转换后文件的下载功能,支持批量下载 |
| 错误处理 | 文件格式验证、文件大小限制(单文件不超过200MB)、转换失败提示 |
项目结构
pdf和图片互转/
├── venv/ # Python虚拟环境
├── app.py # 主应用程序
├── requirements.txt # 依赖清单
├── 启动应用.bat # Windows启动脚本
└── 开发教程.md # 开发文档
技术架构
技术栈
| 技术 | 用途 | 版本要求 |
|---|---|---|
| Python | 开发语言 | >= 3.8 |
| Streamlit | Web框架 | >= 1.28.0 |
| pdf2image | PDF转图片 | >= 1.16.0 |
| Pillow | 图片处理 | >= 10.0.0 |
| img2pdf | 图片转PDF | >= 0.5.0 |
| Poppler | PDF渲染引擎 | 外部依赖 |
架构图
┌─────────────────────────────────────────────────────────────┐
│ Streamlit Web 界面 │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ PDF转图片模块 │ │ 图片转PDF模块 │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ pdf2image │ │ img2pdf │ │
│ │ (包装器) │ │ (纯Python) │ │
│ └────────┬────────┘ └─────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Poppler │ ← 需要外部安装 │
│ │ (PDF渲染引擎) │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
环境搭建
步骤1:创建虚拟环境
powershell
# 进入项目目录
cd d:\原创\自研软件\pdf和图片互转
# 创建虚拟环境
python -m venv venv
步骤2:激活虚拟环境
Windows PowerShell:
powershell
.\venv\Scripts\Activate
Windows CMD:
cmd
.\venv\Scripts\activate.bat
Linux/Mac:
bash
source venv/bin/activate
步骤3:安装依赖
powershell
# 安装核心依赖
pip install streamlit pdf2image Pillow img2pdf
# 或使用 requirements.txt
pip install -r requirements.txt
requirements.txt 内容:
streamlit>=1.28.0
pdf2image>=1.16.0
Pillow>=10.0.0
img2pdf>=0.5.0
步骤4:安装 Poppler(PDF转图片必需)
方法1:使用 Conda(推荐)
powershell
# 如果已安装 Anaconda 或 Miniconda
conda install -c conda-forge poppler
方法2:手动安装(Windows)
- 从 poppler-windows releases 下载最新版本
- 解压到指定目录,如
C:\Program Files\poppler - 将
bin目录添加到系统 PATH 环境变量:- 右键"此电脑" → 属性 → 高级系统设置 → 环境变量
- 在"系统变量"中找到
Path,点击编辑 - 添加路径:
C:\Program Files\poppler\Library\bin
方法3:使用 Chocolatey(Windows包管理器)
powershell
choco install poppler
步骤5:验证安装
powershell
# 验证 pdftoppm 命令
pdftoppm -v
# 预期输出:
# pdftoppm version 24.xx.x
# Copyright 2005-2024 The Poppler Developers
步骤6:运行应用
方式1:命令行启动
powershell
streamlit run app.py
方式2:使用启动脚本(Windows)
双击运行 启动应用.bat 文件,脚本内容如下:
batch
@echo off
chcp 65001 >nul 2>&1
title PDF Image Converter
echo ========================================
echo PDF Image Converter Starting...
echo ========================================
echo.
cd /d "%~dp0"
if not exist "venv\Scripts\python.exe" (
echo [ERROR] Virtual environment not found!
echo Please run: python -m venv venv
echo.
pause
exit /b 1
)
echo [INFO] Activating virtual environment...
call venv\Scripts\activate.bat
echo [INFO] Starting Streamlit app...
echo [INFO] Browser will open http://localhost:8501
echo.
echo [TIP] Press Ctrl+C to stop
echo ========================================
echo.
venv\Scripts\python.exe -m streamlit run app.py
pause
注意事项:
- 脚本使用英文提示,避免中文乱码问题
- 直接使用
venv\Scripts\python.exe运行,确保使用虚拟环境的Python- 不要使用
streamlit run app.py,因为激活虚拟环境后可能仍使用系统Python
应用将在浏览器中打开:http://localhost:8501
核心功能实现
1. 文件验证模块
python
MAX_FILE_SIZE = 200 * 1024 * 1024 # 200MB
ALLOWED_IMAGE_TYPES = ['jpg', 'jpeg', 'png', 'bmp', 'gif', 'tiff']
def validate_file_size(file_size: int, file_name: str) -> Tuple[bool, str]:
"""验证文件大小"""
if file_size > MAX_FILE_SIZE:
return False, f"文件 '{file_name}' 大小超过200MB限制"
return True, ""
def validate_file_type(file_name: str, allowed_types: List[str]) -> Tuple[bool, str]:
"""验证文件类型"""
file_ext = file_name.lower().split('.')[-1] if '.' in file_name else ''
if file_ext not in allowed_types:
return False, f"文件格式不支持。支持的格式: {', '.join(allowed_types)}"
return True, ""
2. PDF转图片功能
python
from pdf2image import convert_from_bytes
from PIL import Image
import io
@st.cache_data
def convert_pdf_to_images(pdf_bytes: bytes, output_format: str, quality: int, dpi: int):
"""
将PDF转换为图片列表
参数:
pdf_bytes: PDF文件的字节数据
output_format: 输出格式 ('png' 或 'jpg')
quality: 图片质量 (10-100)
dpi: 分辨率 (72-300)
"""
images = convert_from_bytes(
pdf_bytes,
dpi=dpi,
fmt=output_format.lower(),
thread_count=4 # 多线程加速
)
return images
def image_to_bytes(image: Image.Image, format: str, quality: int) -> bytes:
"""将PIL Image对象转换为字节数据"""
img_byte_arr = io.BytesIO()
save_format = 'JPEG' if format.lower() in ['jpg', 'jpeg'] else format.upper()
if save_format == 'JPEG':
image = image.convert('RGB') # JPEG不支持透明通道
image.save(img_byte_arr, format=save_format, quality=quality, optimize=True)
return img_byte_arr.getvalue()
3. 图片转PDF功能
python
import img2pdf
from PIL import Image
import io
def images_to_pdf(image_files: list, page_orientation: str) -> bytes:
"""
将多张图片合并为PDF
参数:
image_files: 图片文件列表
page_orientation: 页面方向 ('自动', '纵向', '横向')
"""
img_bytes_list = []
for file in image_files:
img = Image.open(file)
# 转换为RGB模式(PDF不支持RGBA)
if img.mode in ('RGBA', 'P'):
img = img.convert('RGB')
# 转换为字节
img_byte_arr = io.BytesIO()
img.save(img_byte_arr, format='JPEG', quality=95)
img_bytes_list.append(img_byte_arr.getvalue())
# 设置页面布局(注意:get_layout_fun 在某些版本可能不可用)
layout_fun = None
try:
if page_orientation == '纵向':
layout_fun = img2pdf.get_layout_fun(
(img2pdf.mm_to_pt(210), img2pdf.mm_to_pt(297)) # A4尺寸
)
elif page_orientation == '横向':
layout_fun = img2pdf.get_layout_fun(
(img2pdf.mm_to_pt(297), img2pdf.mm_to_pt(210))
)
except AttributeError:
pass # 如果方法不存在,使用默认布局
# 生成PDF
if layout_fun is not None:
pdf_bytes = img2pdf.convert(img_bytes_list, layout_fun=layout_fun)
else:
pdf_bytes = img2pdf.convert(img_bytes_list)
return pdf_bytes
4. 批量下载功能
python
import zipfile
def create_zip_file(files: List[Tuple[str, bytes]]) -> bytes:
"""
将多个文件打包为ZIP
参数:
files: 文件列表,每个元素为 (文件名, 文件字节) 元组
"""
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for file_name, file_bytes in files:
zip_file.writestr(file_name, file_bytes)
return zip_buffer.getvalue()
5. Streamlit 界面实现
python
import streamlit as st
st.set_page_config(
page_title="PDF与图片互转工具",
page_icon="📄",
layout="wide"
)
# 文件上传(注意:必须指定唯一的 key)
uploaded_file = st.file_uploader(
"上传PDF文件",
type=['pdf'],
help="支持PDF格式,单文件最大200MB",
key="pdf_uploader" # 唯一标识
)
# 转换设置(每个控件都需要唯一的 key)
output_format = st.selectbox(
"输出格式",
options=['PNG', 'JPG'],
key="output_format_select"
)
quality = st.slider(
"图片质量",
min_value=10,
max_value=100,
value=85,
key="quality_slider"
)
dpi = st.select_slider(
"分辨率",
options=[72, 96, 150, 200, 300],
value=150,
key="dpi_slider"
)
# 转换按钮(注意:按钮也需要唯一的 key)
if st.button("开始转换", type="primary", key="pdf_to_image_btn"):
with st.spinner("正在转换中..."):
progress_bar = st.progress(0) # 注意:progress 不支持 key 参数
# 执行转换逻辑
progress_bar.progress(100)
st.success("转换成功!")
# 下载按钮(每个下载按钮也需要唯一的 key)
st.download_button(
label="下载文件",
data=file_bytes,
file_name="converted.pdf",
mime="application/pdf",
key="download_pdf_result" # 唯一标识
)
关键技术解析
为什么 PDF转图片需要 Poppler?
pdf2image 的工作原理
pdf2image 库本身并不包含 PDF 渲染引擎,它只是一个包装器(wrapper):
Python代码 → pdf2image库 → 调用poppler命令行工具 → 渲染PDF → 返回图片
Poppler 是什么?
Poppler 是一个开源的 PDF 渲染库,基于 Xpdf 项目开发。它提供了:
| 工具 | 功能 |
|---|---|
pdftoppm |
将PDF页面转换为PPM图片 |
pdftocairo |
使用Cairo库渲染PDF(支持更多格式) |
pdftotext |
提取PDF文本 |
pdfinfo |
显示PDF信息 |
为什么不内置到 pdf2image?
| 原因 | 说明 |
|---|---|
| 跨平台兼容 | Poppler 是 C/C++ 编写的原生库,不同操作系统需要不同编译版本 |
| 体积问题 | Poppler 完整包约 50-100MB,不适合打包进 Python 库 |
| 许可证 | Poppler 使用 GPL 许可证,与某些商业场景有冲突 |
| 维护分离 | PDF渲染是复杂任务,由专业团队维护更合适 |
为什么图片转PDF不需要额外依赖?
img2pdf 库不需要外部工具,因为:
- 它只是将图片嵌入PDF容器中
- 不需要渲染复杂的PDF内容
- Python 的 Pillow 库就能处理图片读取
Streamlit 元素 ID 机制
Streamlit 会根据元素类型和参数自动生成内部 ID。如果同一页面存在多个相同类型和参数的元素,会导致 ID 冲突。
解决方案 :为每个交互元素指定唯一的 key 参数:
python
# ❌ 错误:两个标签页中有相同的按钮,会导致 ID 冲突
st.button("开始转换") # 在 tab1 中
st.button("开始转换") # 在 tab2 中 - 冲突!
# ✅ 正确:使用唯一的 key 区分
st.button("开始转换", key="tab1_convert_btn")
st.button("开始转换", key="tab2_convert_btn")
注意事项:
st.progress()不支持key参数st.spinner()不支持key参数st.empty()不支持key参数
性能优化技巧
1. 使用缓存
python
@st.cache_data
def convert_pdf_to_images(pdf_bytes, output_format, quality, dpi):
# 缓存转换结果,避免重复转换
return convert_from_bytes(pdf_bytes, dpi=dpi, fmt=output_format)
2. 多线程处理
python
# pdf2image 支持多线程
images = convert_from_bytes(pdf_bytes, thread_count=4)
3. 分辨率选择建议
| DPI | 适用场景 | 文件大小 | 转换速度 |
|---|---|---|---|
| 72 | 网页预览 | 最小 | 最快 |
| 96 | 屏幕显示 | 小 | 快 |
| 150 | 一般文档 | 中等 | 中等 |
| 200 | 打印预览 | 较大 | 较慢 |
| 300 | 高质量打印 | 最大 | 最慢 |
常见问题与解决方案
问题1:PDF转图片报错 "Unable to get page count"
原因:未安装 Poppler 或 PATH 配置不正确
解决方案:
powershell
# 验证安装
pdftoppm -v
# 如果命令不存在,重新安装 Poppler
conda install -c conda-forge poppler
问题2:PNG图片转PDF后颜色异常
原因:PNG支持透明通道,但PDF/JPEG不支持
解决方案:
python
from PIL import Image
img = Image.open(file)
if img.mode in ('RGBA', 'P'):
img = img.convert('RGB') # 转换为RGB模式
问题3:StreamlitDuplicateElementId 错误
错误信息:
StreamlitDuplicateElementId: There are multiple button elements with the same auto-generated ID.
原因 :多个交互元素具有相同的标签但没有唯一的 key
解决方案:
python
# 为每个元素添加唯一的 key
st.button("开始转换", key="pdf_to_image_btn")
st.button("开始转换", key="image_to_pdf_btn")
st.selectbox("输出格式", options=['PNG', 'JPG'], key="output_format_select")
st.selectbox("页面大小", options=['A4', 'A3'], key="page_size_select")
问题4:TypeError: ProgressMixin.progress() got an unexpected keyword argument 'key'
原因 :st.progress() 不支持 key 参数
解决方案:
python
# ❌ 错误
progress_bar = st.progress(0, key="my_progress")
# ✅ 正确
progress_bar = st.progress(0)
问题5:img2pdf 转换失败 "'NoneType' object is not callable"
原因 :img2pdf.get_layout_fun() 在某些版本中可能不可用或返回 None
解决方案:
python
layout_fun = None
try:
if page_orientation == '纵向':
layout_fun = img2pdf.get_layout_fun((img2pdf.mm_to_pt(210), img2pdf.mm_to_pt(297)))
except AttributeError:
pass # 方法不存在时使用默认布局
# 安全调用
if layout_fun is not None:
pdf_bytes = img2pdf.convert(img_bytes_list, layout_fun=layout_fun)
else:
pdf_bytes = img2pdf.convert(img_bytes_list)
问题6:大文件转换超时
原因:文件过大或DPI设置过高
解决方案:
- 降低DPI设置
- 分批处理大文件
- 增加Streamlit超时配置
问题7:中文文件名乱码
原因:编码问题
解决方案:
python
from urllib.parse import quote
# 下载时处理文件名
st.download_button(
label="下载文件",
data=file_bytes,
file_name=quote(filename), # URL编码
mime="application/pdf"
)
问题8:验证 Poppler 安装
方法1:命令行验证
powershell
pdftoppm -v
方法2:Python验证
python
from pdf2image import convert_from_path
import tempfile
import os
# 创建测试PDF
import fitz
doc = fitz.open()
page = doc.new_page()
page.insert_text((100, 100), "Test")
pdf_bytes = doc.tobytes()
# 测试转换
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
f.write(pdf_bytes)
temp_path = f.name
try:
images = convert_from_path(temp_path)
print(f"✅ 成功!转换了 {len(images)} 页")
except Exception as e:
print(f"❌ 失败: {e}")
finally:
os.unlink(temp_path)
问题9:启动脚本中文乱码
错误信息:
'PDF涓庡浘鐗囦簰杞伐鍏?鍚姩涓?..' 不是内部或外部命令
原因:批处理文件编码问题,CMD默认使用GBK编码,UTF-8中文会乱码
解决方案:
batch
# 使用英文提示,避免编码问题
echo [INFO] Starting Streamlit app...
# 或者确保文件保存为 ANSI/GBK 编码
问题10:ModuleNotFoundError: No module named 'img2pdf'
错误信息:
ModuleNotFoundError: No module named 'img2pdf'
原因:启动脚本未正确使用虚拟环境,使用了系统Python
解决方案:
batch
# ❌ 错误:可能使用系统Python
call venv\Scripts\activate.bat
streamlit run app.py
# ✅ 正确:直接指定虚拟环境的Python
venv\Scripts\python.exe -m streamlit run app.py
项目扩展建议
1. 替代方案:使用 PyMuPDF
如果不想安装 Poppler,可以使用 PyMuPDF(fitz)替代:
powershell
pip install PyMuPDF
python
import fitz # PyMuPDF
def pdf_to_images_with_fitz(pdf_bytes, output_format='png', dpi=150):
"""使用PyMuPDF转换PDF为图片"""
doc = fitz.open(stream=pdf_bytes, filetype='pdf')
images = []
for page_num in range(len(doc)):
page = doc[page_num]
# 设置缩放比例
zoom = dpi / 72
mat = fitz.Matrix(zoom, zoom)
# 渲染页面为图片
pix = page.get_pixmap(matrix=mat)
# 转换为PIL Image
if output_format.lower() == 'png':
img_data = pix.tobytes('png')
else:
img_data = pix.tobytes('jpeg')
img = Image.open(io.BytesIO(img_data))
images.append(img)
doc.close()
return images
2. 添加OCR功能
powershell
pip install pytesseract
python
import pytesseract
def extract_text_from_image(image):
"""从图片中提取文字"""
return pytesseract.image_to_string(image, lang='chi_sim+eng')
3. 添加PDF合并/拆分功能
python
import fitz
def merge_pdfs(pdf_files):
"""合并多个PDF"""
merged = fitz.open()
for pdf_file in pdf_files:
doc = fitz.open(stream=pdf_file, filetype='pdf')
merged.insert_pdf(doc)
doc.close()
return merged.tobytes()
def split_pdf(pdf_bytes, page_ranges):
"""拆分PDF"""
doc = fitz.open(stream=pdf_bytes, filetype='pdf')
new_doc = fitz.open()
for page_num in page_ranges:
new_doc.insert_pdf(doc, from_page=page_num, to_page=page_num)
return new_doc.tobytes()
4. 添加水印功能
python
import fitz
def add_watermark(pdf_bytes, watermark_text):
"""添加水印"""
doc = fitz.open(stream=pdf_bytes, filetype='pdf')
for page in doc:
# 创建水印
rect = page.rect
text = page.insert_text(
(rect.width/2, rect.height/2),
watermark_text,
fontsize=50,
color=(0.8, 0.8, 0.8),
rotate=45
)
return doc.tobytes()
5. 部署到云端
Streamlit Cloud 部署:
-
创建
packages.txt文件(用于安装系统依赖):poppler-utils
-
推送到 GitHub
-
在 Streamlit Cloud 部署
开发注意事项总结
Streamlit 开发要点
| 要点 | 说明 |
|---|---|
| 唯一 key | 所有交互元素(button、selectbox、slider等)必须指定唯一的 key 参数 |
| progress 无 key | st.progress() 不支持 key 参数,不要添加 |
| session_state | 使用 st.session_state 在页面刷新间保存数据 |
| 缓存装饰器 | 使用 @st.cache_data 缓存耗时操作结果 |
img2pdf 使用要点
| 要点 | 说明 |
|---|---|
| 布局函数 | get_layout_fun() 可能不可用,需要 try-except 处理 |
| RGB模式 | PNG等透明图片需转换为RGB模式 |
| 字节输入 | img2pdf.convert() 接受字节列表作为输入 |
pdf2image 使用要点
| 要点 | 说明 |
|---|---|
| Poppler依赖 | 必须安装 Poppler 并配置 PATH |
| 多线程 | 使用 thread_count 参数加速转换 |
| 内存管理 | 大文件转换可能占用大量内存,注意监控 |
总结
本教程详细介绍了 PDF 与图片互转工具的完整开发过程,包括:
- ✅ 环境搭建与依赖安装
- ✅ PDF转图片功能实现
- ✅ 图片转PDF功能实现
- ✅ 文件验证与错误处理
- ✅ 批量下载功能
- ✅ Poppler 依赖原理详解
- ✅ Streamlit 元素 ID 机制与解决方案
- ✅ 常见问题解决方案
- ✅ 项目扩展建议
通过本教程,你可以快速搭建一个功能完善的 PDF 与图片互转 Web 应用,并根据实际需求进行扩展。