
wechat_2025-06-28_104719_437.png (35.33 KB, 下载次数: 0)
下载附件
2025-6-28 10:47 上传
源码:
[Python] 纯文本查看 复制代码import argparse
from PIL import Image
import img2pdf
import os
import sys
import traceback
import tempfile
import shutil
import uuid
import logging
import locale
import math
# 设置系统区域设置以支持中文路径
def set_system_locale():
try:
locale.setlocale(locale.LC_ALL, 'zh_CN.UTF-8')
except:
try:
locale.setlocale(locale.LC_ALL, '')
except:
pass
# 确保路径使用正确编码
def safe_path(path):
"""处理中文路径问题"""
if isinstance(path, bytes):
return path.decode(sys.getfilesystemencoding())
return path
def setup_logger(enable_file_logging=True):
"""创建日志系统"""
# 创建日志器
logger = logging.getLogger('img2pdf_converter')
logger.setLevel(logging.DEBUG)
# 创建控制台处理器(始终启用)
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
logger.addHandler(console_handler)
# 创建文件处理器(可选)
if enable_file_logging:
# 创建日志目录
log_dir = os.path.join(os.path.expanduser("~"), "Desktop", "img2pdf_logs")
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, "conversion_log.txt")
# 文件处理器
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
logger.addHandler(file_handler)
logger.info(f"日志文件已启用: {log_file}")
else:
logger.info("日志文件已禁用")
return logger
def create_a4_layout(paper_size):
#修改PDF尺寸
if paper_size == "A4":
"""创建高质量A4布局函数,确保图片宽度自适应A4"""
# A4尺寸 (210mm x 297mm)
width = img2pdf.mm_to_pt(210)
height = img2pdf.mm_to_pt(297)
elif paper_size == "A5":
# A5尺寸 (148mm x 210mm)
width = img2pdf.mm_to_pt(148)
height = img2pdf.mm_to_pt(210)
else:
# 默认使用A4
width = img2pdf.mm_to_pt(210)
height = img2pdf.mm_to_pt(297)
return img2pdf.get_layout_fun(
pagesize=(width, height),
auto_orient=True,
fit=img2pdf.FitMode.into, # 确保图片自适应A4页面
border=None
)
def calculate_a4_dimensions(paper_size,dpi=300):
"""计算A4尺寸对应的像素值"""
mm_to_inch = 1 / 25.4
#修改PDF尺寸
if paper_size == "A4":
a4_width_mm = 210
a4_height_mm = 297
elif paper_size == "A5":
a4_width_mm = 148
a4_height_mm = 210
else:
# 默认使用A4
a4_width_mm = 210
a4_height_mm = 297
return (
int(a4_width_mm * mm_to_inch * dpi),
int(a4_height_mm * mm_to_inch * dpi)
)
def process_image_for_a4(image_path, logger,paper_size):
"""处理单张图片以适应A4尺寸:宽度缩放到A4宽度,高度按比例缩放,左上角对齐,空白区域填充白色"""
try:
# 计算A4尺寸
a4_width_px, a4_height_px = calculate_a4_dimensions(paper_size)
logger.info(f" {paper_size}尺寸: {a4_width_px}x{a4_height_px} 像素 (300DPI)")
# 打开原始图片
with Image.open(image_path) as orig_img:
logger.info(f" 原始尺寸: {orig_img.size}, 格式: {orig_img.format}, 模式: {orig_img.mode}")
# 处理透明通道
if orig_img.mode in ['RGBA', 'LA']:
logger.info(" 转换透明通道为RGB...")
background = Image.new('RGB', orig_img.size, (255, 255, 255))
background.paste(orig_img, mask=orig_img.split()[3] if orig_img.mode == 'RGBA' else orig_img)
img = background
elif orig_img.mode != 'RGB':
logger.info(" 转换为RGB...")
img = orig_img.convert('RGB')
else:
img = orig_img.copy()
# 创建A4尺寸的白色背景
background = Image.new('RGB', (a4_width_px, a4_height_px), (255, 255, 255))
#转换接口
#PDF_type=1,宽度根据A4宽度适配,高度以A4高度为准,但是图片高度大于A4高度,图片会显示不全
#PDF_type=2,宽度根据A4宽度适配缩放,图片高度大于A4高度时图片宽度会缩放背景会有白色
#PDF_type=3,宽度铺满,图片高度小于PDF高度根据图片原始高度适配,大于PDF高度图片铺满高度
#PDF_type=0,宽度高度都铺满A4
PDF_type=3
if PDF_type == 1:
# 计算缩放后的高度(保持宽高比)
scale_ratio = a4_width_px / img.width
new_height = int(img.height * scale_ratio)
logger.info(f" 缩放图片: {img.width}x{img.height} -> {a4_width_px}x{new_height}")
img = img.resize((a4_width_px, new_height), Image.LANCZOS)
# 将缩放后的图片粘贴到背景左上角
background.paste(img, (0, 0))
elif PDF_type == 2:
# 计算缩放比例(确保图片完整显示在A4内)
width_ratio = a4_width_px / img.width
height_ratio = a4_height_px / img.height
scale_ratio = min(width_ratio, height_ratio) # 关键修复:选择较小的比例
new_width = int(img.width * scale_ratio)
new_height = int(img.height * scale_ratio)
logger.info(f" 缩放图片: {img.width}x{img.height} -> {new_width}x{new_height}")
img = img.resize((new_width, new_height), Image.LANCZOS)
# 居中放置图片(计算粘贴位置)
paste_x = (a4_width_px - new_width) // 2
paste_y = (a4_height_px - new_height) // 2
# 将缩放后的图片粘贴到背景中央
background.paste(img, (paste_x, paste_y))
elif PDF_type == 3:
# 计算按宽度缩放后的高度
scaled_height = int(img.height * (a4_width_px / img.width))
# 根据高度比例决定最终高度
if scaled_height > a4_height_px:
# 高度比例大于A4高度 - 高度铺满A4高度
final_height = a4_height_px
# 计算按高度铺满的宽度(用于居中)
scaled_width = int(img.width * (a4_height_px / img.height))
paste_x = (a4_width_px - scaled_width) // 2
logger.info(f" 高度比例大于A4 - 高度铺满: 缩放尺寸 {scaled_width}x{final_height}")
#如果图片宽度要根据高度匹配A4,请把下面的a4_width_px换成paste_x ,下面的background.paste(scaled_img, (paste_x, 0))
scaled_img = img.resize((a4_width_px, final_height), Image.LANCZOS)
# 将图片粘贴到背景中央
#background.paste(scaled_img, (paste_x, 0))
background.paste(scaled_img, (0, 0))
else:
# 高度比例小于A4高度 - 高度按比例自适应
final_height = scaled_height
paste_y = (a4_height_px - final_height) // 2 # 垂直居中
logger.info(f" 高度比例小于A4 - 高度自适应: 缩放尺寸 {a4_width_px}x{final_height}")
scaled_img = img.resize((a4_width_px, final_height), Image.LANCZOS)
# 将图片粘贴到背景上方居中位置
background.paste(scaled_img, (0, paste_y))
else:
# 直接缩放图片到A4尺寸(不保持宽高比)
logger.info(f" 缩放图片: {img.width}x{img.height} -> {a4_width_px}x{a4_height_px}")
img = img.resize((a4_width_px, a4_height_px), Image.LANCZOS)
background.paste(img, (0, 0))
return background, (300, 300)
except Exception as e:
logger.error(f" 处理图片失败: {str(e)}")
logger.debug(traceback.format_exc())
raise
def process_and_save_image(image_path, temp_dir, logger, paper_size="A4"):
"""处理单张图片并保存到临时文件"""
try:
# 处理图片
if paper_size == "A4":
img, dpi = process_image_for_a4(image_path, logger,paper_size)
else:
with Image.open(image_path) as orig_img:
logger.info(f" 原始尺寸: {orig_img.size}, 格式: {orig_img.format}, 模式: {orig_img.mode}")
# 处理透明通道
if orig_img.mode in ['RGBA', 'LA']:
logger.info(" 转换透明通道为RGB...")
background = Image.new('RGB', orig_img.size, (255, 255, 255))
background.paste(orig_img, mask=orig_img.split()[3] if orig_img.mode == 'RGBA' else orig_img)
img = background
elif orig_img.mode != 'RGB':
logger.info(" 转换为RGB...")
img = orig_img.convert('RGB')
else:
img = orig_img.copy()
dpi = (300, 300)
# 生成唯一临时文件名
temp_file = os.path.join(temp_dir, f"temp_{uuid.uuid4().hex}.jpg")
# 保存为JPEG格式(最高质量)
img.save(temp_file, format='JPEG', quality=100, subsampling=0, dpi=dpi)
logger.info(f" 图片处理成功,保存到: {temp_file}")
return temp_file
except Exception as e:
logger.error(f" 处理图片失败: {str(e)}")
logger.debug(traceback.format_exc())
return None
def convert_images_to_pdf(image_paths, pdf_path, logger, paper_size="A4"):
"""将多张图片转换为单个PDF文件(高质量)"""
temp_dir = None
files_to_convert = []
success_count = 0
pdf_created = False
#固定PDF尺寸
paper_size="A4"
try:
# 确保路径编码正确
pdf_path = safe_path(pdf_path)
image_paths = [safe_path(p) for p in image_paths]
logger.info(f"输出PDF路径: {pdf_path}")
logger.info(f"纸张大小: {paper_size}")
logger.info(f"输入图片列表: {image_paths}")
# 确保所有图片都存在
for img_path in image_paths:
if not os.path.exists(img_path):
logger.error(f"图片不存在: {img_path}")
return False
else:
logger.info(f"图片存在: {img_path}")
# 确保输出目录存在
output_dir = os.path.dirname(pdf_path)
if output_dir and not os.path.exists(output_dir):
logger.info(f"创建输出目录: {output_dir}")
try:
os.makedirs(output_dir, exist_ok=True)
logger.info(f"目录创建成功")
except Exception as e:
logger.error(f"创建目录失败: {str(e)}")
return False
# 创建临时目录
temp_dir = tempfile.mkdtemp(prefix="img2pdf_temp_")
logger.info(f"临时目录创建成功: {temp_dir}")
# 处理每张图片并保存到临时文件
for i, image_path in enumerate(image_paths):
logger.info(f"[{i+1}/{len(image_paths)}] 处理图片: {os.path.basename(image_path)}")
temp_file = process_and_save_image(image_path, temp_dir, logger, paper_size)
if temp_file:
files_to_convert.append(temp_file)
success_count += 1
if success_count == 0:
logger.error("所有图片处理失败,无法生成PDF")
return False
logger.info(f"准备合并 {success_count} 张图片为PDF...")
# 设置布局函数
layout_fun = None
#paper_size == "A4":
if paper_size != "Original":
try:
#layout_fun = create_a4_layout()
#logger.info("使用A4纸张大小布局(图片宽度自适应)")
layout_fun = create_a4_layout(paper_size)
logger.info(f"使用{paper_size}纸张大小布局")
except Exception as e:
#logger.error(f"创建A4布局失败: {str(e)}")
logger.error(f"创建{paper_size}布局失败: {str(e)}")
return False
# 转换为PDF(关键步骤)
try:
# 先将PDF内容写入内存
if layout_fun:
pdf_bytes = img2pdf.convert(
files_to_convert,
layout_fun=layout_fun,
compression=None # 禁用JPEG压缩
)
else:
pdf_bytes = img2pdf.convert(
files_to_convert,
compression=None # 禁用JPEG压缩
)
# 只有在转换成功后才写入文件
with open(pdf_path, "wb") as pdf_file:
pdf_file.write(pdf_bytes)
pdf_created = True
except Exception as e:
logger.error(f"PDF写入失败: {str(e)}")
logger.error(traceback.format_exc()) # 添加详细错误跟踪
return False
# 验证PDF是否创建成功
if os.path.exists(pdf_path) and pdf_created:
logger.info(f"✅ PDF生成成功: {pdf_path}")
logger.info(f"文件大小: {os.path.getsize(pdf_path)/1024:.2f} KB")
return True
else:
logger.error("❌ PDF文件未创建成功")
return False
except Exception as e:
logger.error(f"❌ 转换失败: {str(e)}")
logger.error(traceback.format_exc())
# 如果转换失败但PDF文件已创建,则删除无效文件
if os.path.exists(pdf_path):
try:
os.remove(pdf_path)
logger.info(f"已删除无效PDF文件: {pdf_path}")
except Exception as e:
logger.error(f"无法删除无效PDF文件: {str(e)}")
return False
finally:
# 清理临时目录
if temp_dir and os.path.exists(temp_dir):
try:
logger.info("清理临时文件...")
shutil.rmtree(temp_dir)
logger.info(f"临时目录已删除: {temp_dir}")
except Exception as e:
logger.error(f"警告: 无法删除临时目录 - {str(e)}")
if __name__ == '__main__':
# 设置系统区域
set_system_locale()
try:
parser = argparse.ArgumentParser(description='将多张图片转换为高质量PDF')
parser.add_argument('image_paths', type=str, nargs='+', help='图片文件的路径(支持多个)')
parser.add_argument('pdf_path', type=str, help='转换后PDF文件的保存路径')
parser.add_argument('--size', type=str, default='A4', choices=['A4', 'Original'],
help='PDF页面大小 (A4: 高质量适配, Original: 保持原始大小) 默认为A4')
parser.add_argument('--log', action='store_true',
help='启用日志文件记录(日志文件将保存在桌面img2pdf_logs目录)')
args = parser.parse_args()
# 设置日志
logger = setup_logger(enable_file_logging=args.log)
logger.info("=" * 50)
logger.info("高质量图片转PDF工具启动")
logger.info(f"命令行参数: {' '.join(sys.argv)}")
logger.info(f"输入图片: {args.image_paths}")
logger.info(f"输出PDF: {args.pdf_path}")
logger.info(f"纸张大小: {args.size}")
logger.info(f"日志文件: {'启用' if args.log else '禁用'}")
logger.info("=" * 50)
success = convert_images_to_pdf(args.image_paths, args.pdf_path, logger, paper_size=args.size)
if not success:
logger.error("❌ 转换失败,未生成PDF文件")
sys.exit(1)
else:
sys.exit(0)
except Exception as e:
# 即使没有日志器也要输出错误
print(f"程序启动失败: {str(e)}")
traceback.print_exc()
sys.exit(1)
打包命令:
[Python] 纯文本查看 复制代码pyinstaller --onefile --hidden-import=img2pdf --hidden-import=PIL --hidden-import=logging --hidden-import=uuid --hidden-import=tempfile --hidden-import=shutil img2pdf.py
转换命令:
[Python] 纯文本查看 复制代码img2pdf.exe "C:\Users\HPT\Desktop\10007.jpg" "C:\Users\HPT\Desktop\123.png" "C:\Users\HPT\Desktop\combined.pdf" --size=A4