import os import platform import shutil import sys import time from pathlib import Path import pandas as pd from pptx import Presentation from pptx.util import Pt def get_system_fonts(): """获取系统中可用的字体列表""" fonts = set() if platform.system() == "Windows": try: # 读取Windows字体目录 system_fonts_dir = Path(os.environ['WINDIR']) / 'Fonts' user_fonts_dir = Path.home() / 'AppData' / 'Local' / 'Microsoft' / 'Windows' / 'Fonts' # 检查系统字体目录 if system_fonts_dir.exists(): for font_file in system_fonts_dir.glob('*.ttf'): fonts.add(font_file.stem) for font_file in system_fonts_dir.glob('*.ttc'): fonts.add(font_file.stem) for font_file in system_fonts_dir.glob('*.otf'): fonts.add(font_file.stem) # 检查用户字体目录 if user_fonts_dir.exists(): for font_file in user_fonts_dir.glob('*.ttf'): fonts.add(font_file.stem) for font_file in user_fonts_dir.glob('*.ttc'): fonts.add(font_file.stem) for font_file in user_fonts_dir.glob('*.otf'): fonts.add(font_file.stem) except Exception as e: print(f"读取系统字体时出错: {e}") # 备选方案:返回常见字体 fonts = {"微软雅黑", "宋体", "黑体", "楷体", "仿宋", "Arial", "Times New Roman", "Courier New", "Microsoft YaHei"} return fonts def is_font_available(font_name): """检查字体是否在系统中可用""" system_fonts = get_system_fonts() # 检查字体名称的多种可能形式 check_names = [font_name, font_name.replace(" ", ""), font_name.replace("-", ""), font_name.lower(), font_name.upper()] for name in check_names: if name in system_fonts: return True return False def install_fonts_from_directory(fonts_dir="fonts"): """从指定目录安装字体到系统""" if platform.system() != "Windows": print("字体安装功能目前仅支持Windows系统") return False # 获取系统字体目录 user_fonts_dir = Path.home() / 'AppData' / 'Local' / 'Microsoft' / 'Windows' / 'Fonts' # 优先使用用户字体目录(不需要管理员权限) target_font_dir = user_fonts_dir # 创建目标目录(如果不存在) target_font_dir.mkdir(parents=True, exist_ok=True) # 检查字体目录是否存在 if not os.path.exists(fonts_dir): print(f"字体目录 {fonts_dir} 不存在,请创建该目录并将字体文件放入") return False # 遍历字体目录中的字体文件 font_extensions = ['.ttf', '.ttc', '.otf', '.fon'] font_files = [] for ext in font_extensions: font_files.extend(Path(fonts_dir).glob(f'*{ext}')) if not font_files: print(f"在 {fonts_dir} 目录中未找到字体文件 (.ttf, .ttc, .otf, .fon)") return False # 安装字体文件 installed_fonts = [] for font_file in font_files: try: # 复制字体文件到系统字体目录 target_path = target_font_dir / font_file.name if not target_path.exists(): # 避免重复安装 shutil.copy2(font_file, target_path) print(f"已安装字体: {font_file.name}") installed_fonts.append(font_file.name) else: pass # print(f"字体已存在: {font_file.name}") # 减少日志输出 except Exception as e: print(f"安装字体 {font_file.name} 时出错: {str(e)}") continue if installed_fonts: print(f"共安装了 {len(installed_fonts)} 个新字体文件") print("注意:新安装的字体可能需要重启Python环境后才能在PowerPoint中使用") return True else: print("没有安装新字体文件,可能已存在于系统中") return False def replace_text_in_slide(prs, slide_index, placeholder, text): """ 在指定幻灯片中替换指定占位符的文本,并保持原有格式。 :param prs: Presentation 对象 :param slide_index: 要操作的幻灯片索引(从0开始) :param placeholder: 占位符名称 (shape.name) :param text: 要替换的文本 """ slide = prs.slides[slide_index] for shape in slide.shapes: if shape.name == placeholder: if not shape.has_text_frame: continue # --- 1. 保存原有格式信息 --- original_paragraph_formats = [] for paragraph in shape.text_frame.paragraphs: paragraph_format = { 'alignment': paragraph.alignment, 'space_before': getattr(paragraph, 'space_before', None), 'space_after': getattr(paragraph, 'space_after', None), 'line_spacing': getattr(paragraph, 'line_spacing', None), # [修复] 补充读取缩进属性,防止后面恢复时 KeyError 'left_indent': getattr(paragraph, 'left_indent', None), 'right_indent': getattr(paragraph, 'right_indent', None), 'first_line_indent': getattr(paragraph, 'first_line_indent', None), 'font_info': [] } for run in paragraph.runs: run_format = { 'font_name': run.font.name, 'font_size': run.font.size, 'bold': run.font.bold, 'italic': run.font.italic, 'underline': run.font.underline, 'color': run.font.color, 'character_space': getattr(run.font, 'space', None), 'all_caps': getattr(run.font, 'all_caps', None), 'small_caps': getattr(run.font, 'small_caps', None) } paragraph_format['font_info'].append(run_format) original_paragraph_formats.append(paragraph_format) # --- 2. 设置新文本内容 --- shape.text = text # --- 3. 恢复原有格式 --- for i, paragraph in enumerate(shape.text_frame.paragraphs): # 如果新文本段落数超过原格式数量,使用最后一个格式或默认格式 orig_idx = i if i < len(original_paragraph_formats) else -1 if not original_paragraph_formats: break # 防止空列表 original_para = original_paragraph_formats[orig_idx] # 恢复段落格式 if original_para['alignment'] is not None: paragraph.alignment = original_para['alignment'] if original_para['space_before'] is not None: paragraph.space_before = original_para['space_before'] if original_para['space_after'] is not None: paragraph.space_after = original_para['space_after'] if original_para['line_spacing'] is not None: paragraph.line_spacing = original_para['line_spacing'] if original_para['left_indent'] is not None: paragraph.left_indent = original_para['left_indent'] if original_para['right_indent'] is not None: paragraph.right_indent = original_para['right_indent'] if original_para['first_line_indent'] is not None: paragraph.first_line_indent = original_para['first_line_indent'] # 恢复字体格式 (尽量应用到所有 runs) # 注意:shape.text = text 会把所有内容变成一个 run,但也可能有多个 for j, run in enumerate(paragraph.runs): # 通常取第一个run的格式,或者按顺序取 font_idx = j if j < len(original_para['font_info']) else 0 if not original_para['font_info']: break original_font = original_para['font_info'][font_idx] # 字体名称 if original_font['font_name'] is not None: font_name = original_font['font_name'] if is_font_available(font_name): run.font.name = font_name else: run.font.name = "微软雅黑" # print(f"警告: 字体 '{font_name}' 不可用,已替换") # 其他属性 if original_font['font_size'] is not None: run.font.size = original_font['font_size'] if original_font['bold'] is not None: run.font.bold = original_font['bold'] if original_font['italic'] is not None: run.font.italic = original_font['italic'] if original_font['underline'] is not None: run.font.underline = original_font['underline'] if original_font['character_space'] is not None: try: run.font.space = original_font['character_space'] except: pass if original_font['all_caps'] is not None: run.font.all_caps = original_font['all_caps'] # 颜色处理 if original_font['color'] is not None: try: # 仅当它是RGB类型时才复制,主题色可能导致报错 if hasattr(original_font['color'], 'rgb'): run.font.color.rgb = original_font['color'].rgb except: pass def replace_picture(prs, slide_index, placeholder, img_path): """ 在指定幻灯片中替换指定占位符的图片。 :param prs: Presentation 对象 :param slide_index: 要操作的幻灯片索引(从0开始) :param placeholder: 占位符名称 :param img_path: 要替换的图片路径 """ if not os.path.exists(img_path): print(f"警告: 图片路径不存在 {img_path}") return slide = prs.slides[slide_index] sp_tree = slide.shapes._spTree # 先找到要替换的形状及其索引位置 for i, shape in enumerate(slide.shapes): if shape.name == placeholder: # 保存旧形状的位置信息 left = shape.left top = shape.top width = shape.width height = shape.height # 从幻灯片中移除旧图片占位符 sp_tree.remove(shape._element) # 添加新图片 new_shape = slide.shapes.add_picture(img_path, left, top, width, height) # 插入新图片到旧形状原来的位置 sp_tree.insert(i, new_shape._element) break if __name__ == "__main__": # --- 1. 资源准备 --- # 安装字体(如果存在fonts目录) if install_fonts_from_directory("../fonts"): print("等待系统识别新安装的字体...") time.sleep(2) source_file = r"../templates/大班幼儿学期发展报告.pptx" output_folder = "output" excel_file = os.path.join("../data/names.xlsx") image_folder = os.path.join("../data/images") # 创建输出文件夹 os.makedirs(output_folder, exist_ok=True) # 检查源文件是否存在 if not os.path.exists(source_file): print(f"错误: 找不到模版文件 {source_file}") sys.exit(1) if not os.path.exists(excel_file): print(f"错误: 找不到数据文件 {excel_file}") sys.exit(1) # --- 2. 定义辅助函数 (显式传入 prs) --- def replace_one_page(current_prs, name, class_name): """替换第一页信息""" replace_text_in_slide(current_prs, 0, "name", name) replace_text_in_slide(current_prs, 0, "class", class_name) def replace_two_page(current_prs, comments, teacher_name): """替换第二页信息""" replace_text_in_slide(current_prs, 1, "comments", comments) replace_text_in_slide(current_prs, 1, "teacher_name", teacher_name) def replace_three_page(current_prs, name, english_name, sex, birthday, zodiac, friend, hobby, game, food, me_image): """替换第三页信息 :param current_prs: Presentation对象,当前演示文稿 :param name: 学生姓名 :param english_name: 学生英文名 :param sex: 性别 :param birthday: 生日 :param zodiac: 生肖 :param friend: 好朋友 :param hobby: 爱好 :param game: 爱玩的游戏 :param food: 爱吃的食物 :param me_image: 自己的照片 """ replace_text_in_slide(current_prs, 2, "name", name) replace_text_in_slide(current_prs, 2, "english_name", english_name) replace_text_in_slide(current_prs, 2, "sex", sex) replace_text_in_slide(current_prs, 2, "birthday", birthday) replace_text_in_slide(current_prs, 2, "zodiac", zodiac) replace_text_in_slide(current_prs, 2, "friend", friend) replace_text_in_slide(current_prs, 2, "hobby", hobby) replace_text_in_slide(current_prs, 2, "game", game) replace_text_in_slide(current_prs, 2, "food", food) replace_picture(current_prs, 2, "me_image", me_image) def replace_four_page(current_prs, class_image): """替换第四页信息""" replace_picture(current_prs, 3, "class_image", class_image) def replace_five_page(current_prs, image1, image2): """替换第五页信息""" replace_picture(current_prs, 4, "image1", image1) replace_picture(current_prs, 4, "image2", image2) # --- 3. 读取数据并处理 --- try: df = pd.read_excel(excel_file, sheet_name="Sheet1") datas = df[["姓名", "英文名", "性别", "生日", "属相", "我的好朋友", "我的爱好", "喜欢的游戏", "喜欢吃的食物", "评价"]].values.tolist() class_name = "K4D" teacher_name = ["简蜜", "王敏千", "李玉香"] print(f"开始处理,共 {len(datas)} 位学生...") # --- 4. 循环处理 --- for i, (name, english_name, sex, birthday, zodiac, friend, hobby, game, food, comments) in enumerate(datas): # 班级图片 print(f"[{i + 1}/{len(datas)}] 正在生成: {name}") # [修复] 每次循环重新加载模版,保证文件干净 prs = Presentation(source_file) # 替换第一页内容 replace_one_page(prs, name, class_name) # 替换第二页内容 teacher_names = " ".join(teacher_name) replace_two_page(prs, comments, teacher_names) # 替换第三页内容 student_image_folder = os.path.join(image_folder, name) if os.path.exists(student_image_folder): me_image = os.path.join(student_image_folder, "me_image.jpg") replace_three_page(prs, name, english_name, sex, birthday, zodiac, friend, hobby, game, food, me_image) else: print(f"错误: 学生图片文件夹不存在 {student_image_folder}") # 替换第四页内容 class_image = os.path.join(image_folder, class_name + ".jpg") if os.path.exists(class_image): replace_four_page(prs, class_image) else: print(f"错误: 班级图片文件不存在 {class_image}") continue # 替换第五页内容 if os.path.exists(student_image_folder): image1 = os.path.join(student_image_folder, "1.jpg") image2 = os.path.join(student_image_folder, "1.jpg") replace_five_page(prs, image1, image2) else: print(f"错误: 学生图片文件夹不存在 {student_image_folder}") # 获取文件拓展名 file_ext = os.path.splitext(source_file)[1] safe_name = str(name).strip() # 去除可能存在的空格 new_filename = f"{class_name} {safe_name} 幼儿成长报告{file_ext}" output_path = os.path.join(output_folder, new_filename) # 保存 try: prs.save(output_path) except PermissionError: print(f"保存失败: 文件 {new_filename} 可能已被打开,请关闭后重试。") print("\n所有报告生成完毕!") except Exception as e: print(f"程序运行出错: {str(e)}") import traceback traceback.print_exc()