Python图片转PDF源码

Python图片转PDF源码

📅 2025年07月15日 👀 121 浏览 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)

打包命令:

pyinstaller --onefile --hidden-import=img2pdf --hidden-import=PIL --hidden-import=logging --hidden-import=uuid --hidden-import=tempfile --hidden-import=shutil  img2pdf.py

转换命令:

img2pdf.exe "C:\Users\HPT\Desktop\10007.jpg" "C:\Users\HPT\Desktop\123.png" "C:\Users\HPT\Desktop\combined.pdf" --size=A4
发布于 2025年07月15日 更新于 2025年11月28日
← 返回首页

评论 (0)

暂无评论,快来发表第一条评论吧!