Python图片转PDF源码

查看 12|回复 1
作者:tyler998   
支持单图和多图合并转PDF输出,默认了A4纸格式输出,可以自定义修改


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

图片, 高度

mytomsummer   

看看能不能用
您需要登录后才可以回帖 登录 | 立即注册

返回顶部