diff --git a/main/make_ppt.py b/main/make_ppt.py new file mode 100644 index 0000000..f7fab17 --- /dev/null +++ b/main/make_ppt.py @@ -0,0 +1,584 @@ +from pptx import Presentation +from pptx.util import Inches, Pt, Cm +from pptx.dml.color import RGBColor +from pptx.enum.text import PP_ALIGN +from pptx.enum.text import MSO_ANCHOR # 导入正确的垂直对齐枚举 +from pptx.enum.shapes import MSO_SHAPE +from pptx.chart.data import ChartData +from pptx.enum.chart import XL_CHART_TYPE,XL_LABEL_POSITION,XL_LEGEND_POSITION +from pptx.table import Table + + +from parse_html import parse_html_to_ppt +from init import gcfg + +def apply_gradient_background(slide): + """应用统一的渐变背景""" + background = slide.background + fill = background.fill + fill.gradient() + fill.gradient_stops[0].color.rgb = RGBColor(0, 32, 96) # 深蓝 + fill.gradient_stops[1].color.rgb = RGBColor(0, 112, 192) # 蓝色 + +def create_content_slide(prs, title_text, subtitle_text=None, content_items=None): + """创建带二级标题的内容页""" + slide_layout = prs.slide_layouts[6] # 空白布局 + slide = prs.slides.add_slide(slide_layout) + apply_gradient_background(slide) + + # 添加主标题 + left = Cm(1.5) + top = Cm(1.5) + width = Cm(30) + height = Cm(2) + title_box = slide.shapes.add_textbox(left, top, width, height) + tf = title_box.text_frame + tf.word_wrap = True + tf.clear() + + p = tf.add_paragraph() + p.text = title_text + p.font.size = Pt(36) + p.font.color.rgb = RGBColor(255, 255, 255) + p.font.bold = True + + # 添加二级标题(如果有) + if subtitle_text: + left = Cm(1.5) + top = Cm(3) + width = Cm(30) + height = Cm(1.5) + subtitle_box = slide.shapes.add_textbox(left, top, width, height) + tf = subtitle_box.text_frame + tf.word_wrap = True + tf.clear() + + p = tf.add_paragraph() + p.text = subtitle_text + p.font.size = Pt(24) + p.font.color.rgb = RGBColor(200, 230, 255) # 浅蓝色 + p.font.italic = True + + # 添加内容区域,多级列表 + if content_items: + # 添加内容文字 + left_text = Cm(2) + top_text = Cm(5.5 if subtitle_text else 4.5) + width_text = Cm(36) + height_text = Cm(8) + text_box = slide.shapes.add_textbox(left_text, top_text, width_text, height_text) + tf = text_box.text_frame + + # 启用自动换行 + tf.word_wrap = True + tf.clear() + + for item in content_items: + p = tf.add_paragraph() + p.text = item["text"] + + p.font.size = Pt(item.get("size", 18)) + p.font.color.rgb = RGBColor(200, 230, 255) + p.space_after = Pt(item.get("space_after", 8)) + p.level = item.get("level", 0) + if item.get("bold", False): + p.font.bold = True + if item.get("bullet", False): + p.text = "• " + p.text + + return slide + +def create_table_slide(prs, title_text, subtitle_text, headers, data,content): + """创建带表格的数据页""" + slide = create_content_slide(prs, title_text, subtitle_text,content) + + # 计算表格位置和大小 + left = Cm(2) + top = Cm(8 if content else 7) #有内容则朝下一些 + width = Cm(26.5) + + height = Cm(12 if len(data) >10 else 8) #表格数据比较多 + + # 创建表格 (行数=数据行数+1,列数=标题数) + rows = len(data) + 1 + cols = len(headers) + table = slide.shapes.add_table(rows, cols, left, top, width, height).table + + # 设置表格样式 + table.first_row = True # 强调第一行 + table.horz_banding = True # 横向条纹 + + # 设置表头 + for col_idx, header in enumerate(headers): + cell = table.cell(0, col_idx) + cell.text = header + cell.fill.solid() + cell.fill.fore_color.rgb = RGBColor(0, 64, 128) # 深蓝色表头 + cell.text_frame.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255) + cell.text_frame.paragraphs[0].font.bold = True + cell.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER + + # 填充表格数据 + for row_idx, row_data in enumerate(data, start=1): + for col_idx, cell_data in enumerate(row_data): + cell = table.cell(row_idx, col_idx) + cell.text = str(cell_data) + cell.fill.solid() + cell.fill.fore_color.rgb = RGBColor(255, 255, 255) + cell.fill.fore_color.alpha = 0.2 # 半透明白色 + cell.text_frame.paragraphs[0].font.color.rgb = RGBColor(0, 0, 0) # 黑色文字 + cell.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER + + # #边框线(赞不支持) + # for border_side in ["top", "bottom", "left", "right"]: + # border = getattr(cell, f"{border_side}_border") + # border.fill.background() # 清除任何填充色,确保只应用线条样式 + # border.color.rgb = RGBColor(0, 64, 128) + # border.size = Cm(0.1) + + # 设置表格边框 + for cell in table.iter_cells(): + cell.margin_left = Cm(0.1) + cell.margin_right = Cm(0.1) + cell.margin_top = Cm(0.05) + cell.margin_bottom = Cm(0.05) + cell.vertical_anchor = MSO_ANCHOR.MIDDLE + + return slide + + +def create_chart_slide(prs, title_text, subtitle_text, chart_type, data): + """创建图表的数据页""" + slide = create_content_slide(prs, title_text, subtitle_text) + + #半透明白色背景 + left_content = Cm(2) + top_content = Cm(7 if subtitle_text else 6) # 根据是否有二级标题调整位置 + width_content = Cm(28.5) + height_content = Cm(15 if len(data) >10 else 12 ) + content_box = slide.shapes.add_shape( + MSO_SHAPE.RECTANGLE, left_content, top_content, width_content, height_content + ) + content_box.fill.solid() + content_box.fill.fore_color.rgb = RGBColor(255, 255, 255) + content_box.fill.fore_color.alpha = 0.2 # 20%透明度 + content_box.line.color.rgb = RGBColor(200, 230, 255) + + # 和前端颜色一致 + colors = [ + "#FF6384", # 对应 rgba(255,99,132,1) + "#36A2EB", # 对应 rgba(54, 162, 235, 1) + "#FFCE56", # 对应 rgba(255, 206, 86, 1) + "#4BC0C0", # 对应 rgba(75, 192, 192, 1) + "#9966FF", # 对应 rgba(153, 102, 255, 1) + "#FF9F40", # 对应 rgba(255, 159, 64, 1) + "#FF6347", # 新增:对应 rgba(255, 99, 71, 1),番茄红 + "#90EE90", # 新增:对应 rgba(144, 238, 144, 1),淡绿 + "#ADD8E6", # 新增:对应 rgba(173, 216, 230, 1),浅天蓝 + "#FFC0CB" # 新增:对应 rgba(255, 192, 203, 1),浅粉 +] + + # 添加图表数据 + if data: + chart_data = ChartData() + + chart_data.categories = data["categories"] + del data["categories"] + for lable, series in data.items(): + chart_data.add_series(lable, series) + + # 添加图表 + left_chart = Cm(3) + top_chart = Cm(8) + width_chart = Cm(25) + height_chart = Cm(12 if len(data) >10 else 10 ) + + if chart_type=="[chart][bar]": + xl_chart_type=XL_CHART_TYPE.COLUMN_CLUSTERED + elif chart_type=="[chart][line]": + xl_chart_type=XL_CHART_TYPE.LINE + elif chart_type=="[chart][bar_line]": + xl_chart_type=XL_CHART_TYPE.BAR_CLUSTERED + elif chart_type=="[chart][area]": + xl_chart_type=XL_CHART_TYPE.AREA + else: + xl_chart_type=XL_CHART_TYPE.BAR_STACKED + + chart = slide.shapes.add_chart( + xl_chart_type,left_chart, top_chart, width_chart, height_chart, chart_data + ) + + # 设置数据标签 + plot = chart.chart.plots[0] + plot.has_data_labels = True + data_labels = plot.data_labels + data_labels.show_value = True # 显示数值 + data_labels.position = XL_LABEL_POSITION.OUTSIDE_END # 标签位置 + # 设置数据标签的字体大小 + data_labels.font.size = Pt(6) # 假设你想要设置字体大小为12磅 + + # 设置图例 + chart.chart.has_legend = True # 确保图例可见 + chart.chart.legend.position = XL_LEGEND_POSITION.BOTTOM # 设置图例位置为底部 + chart.chart.legend.include_in_layout = False # 不让图例覆盖图表 + + # 设置图表颜色 + if len(chart.chart.series) ==1: #只有一个series + for idx, point in enumerate(chart.chart.series[0].points): + if idx < len(colors): # 确保颜色数量足够 + fill = point.format.fill + fill.solid() + fill.fore_color.alpha = 0.2 # 20%透明度 + fill.fore_color.rgb = RGBColor.from_string(colors[idx][1:]) + else: + #设置图表颜色 - 为每个系列分配一种颜色 + for idx, serie in enumerate(chart.chart.series): + if idx < len(colors): # 确保颜色数量足够 + fill = serie.format.fill + fill.solid() + fill.fore_color.rgb = RGBColor.from_string(colors[idx][1:]) # 移除前缀'#' + + # # 设置图表颜色以匹配主题 + # chart.chart.plots[0].series[0].format.fill.solid() + # chart.chart.plots[0].series[0].format.fill.fore_color.rgb = RGBColor(100, 180, 255) + # chart.chart.plots[0].series[1].format.fill.solid() + # chart.chart.plots[0].series[1].format.fill.fore_color.rgb = RGBColor(200, 230, 255) + + return slide + + + + +def create_image_layout_slide(prs, title_text, subtitle_text, images,content_items=None): + """创建图片布局页面""" + slide = create_content_slide(prs, title_text, subtitle_text) + + # 清除默认内容区域 + for shape in slide.shapes: + if shape.shape_type == MSO_SHAPE.ROUNDED_RECTANGLE: + sp = shape._element + sp.getparent().remove(sp) + + # 根据图片数量决定布局 + if len(images) == 1: + # 单张大图布局 + left = Cm(3) + top = Cm(5 if subtitle_text else 4)+Cm(0.5) + width = Cm(26) + height = Cm(15) + pic = slide.shapes.add_picture(images[0]["path"], left, top, width, height) + + # 添加图片说明 + if "caption" in images[0]: + left_cap = Cm(3) + top_cap = Cm(19 if subtitle_text else 20) + width_cap = Cm(25) + height_cap = Cm(1) + cap = slide.shapes.add_textbox(left_cap, top_cap, width_cap, height_cap) + tf = cap.text_frame + p = tf.add_paragraph() + if images[0]["caption"]: + p.text = images[0]["caption"] + else: + p.text = "主图" + p.font.size = Pt(14) + p.font.color.rgb = RGBColor(200, 230, 255) + p.alignment = PP_ALIGN.CENTER + + # 在图片的右侧添加内容区域,可以多级列表 + if content_items: + # 添加内容文字 + left_text = Cm(30) + top_text = Cm(5.5 if subtitle_text else 4.5) + width_text = Cm(5) + height_text = Cm(18) + text_box = slide.shapes.add_textbox(left_text, top_text, width_text, height_text) + tf = text_box.text_frame + + # 启用自动换行 + tf.word_wrap = True + tf.clear() + + for item in content_items: + p = tf.add_paragraph() + p.text = item["text"] + p.font.size = Pt(item.get("size", 18)) + p.font.color.rgb = RGBColor(200, 230, 255) + p.space_after = Pt(item.get("space_after", 8)) + p.level = item.get("level", 0) + if item.get("bold", False): + p.font.bold = True + if item.get("bullet", False): + p.text = "• " + p.text + + elif len(images) == 2: + # 两张图片并排布局,大小一致 + # 第一张图片 + left1 = Cm(2) + top1 = Cm(5 if subtitle_text else 4)+Cm(0.5) + width1 = Cm(13) + height1 = Cm(15) + pic1 = slide.shapes.add_picture(images[0]["path"], left1, top1, width1, height1) + + # 第二张图片 + left2 = Cm(16) + top2 = Cm(5 if subtitle_text else 4)+Cm(0.5) + width2 = Cm(13) + height2 = Cm(15) + pic2 = slide.shapes.add_picture(images[1]["path"], left2, top2, width2, height2) + + # 添加图片说明 + for i, img in enumerate(images): + if "caption" in img: + left_cap = Cm(2 if i == 0 else 19) + top_cap = Cm(17 if subtitle_text else 16) + width_cap = Cm(13) + height_cap = Cm(1) + cap = slide.shapes.add_textbox(left_cap, top_cap, width_cap, height_cap) + tf = cap.text_frame + p = tf.add_paragraph() + if img["caption"]: + p.text = img["caption"] + else: + p.text = f"图片{i+1}" + p.font.size = Pt(12) + p.font.color.rgb = RGBColor(200, 230, 255) + p.alignment = PP_ALIGN.CENTER + + # 在图片的右侧添加内容区域,可以多级列表 + if content_items: + # 添加内容文字 + left_text = Cm(30) + top_text = Cm(5.5 if subtitle_text else 4.5) + width_text = Cm(5) + height_text = Cm(18) + text_box = slide.shapes.add_textbox(left_text, top_text, width_text, height_text) + tf = text_box.text_frame + + # 启用自动换行 + tf.word_wrap = True + tf.clear() + + for item in content_items: + p = tf.add_paragraph() + p.text = item["text"] + p.font.size = Pt(item.get("size", 18)) + p.font.color.rgb = RGBColor(200, 230, 255) + p.space_after = Pt(item.get("space_after", 8)) + p.level = item.get("level", 0) + if item.get("bold", False): + p.font.bold = True + if item.get("bullet", False): + p.text = "• " + p.text + + + elif len(images) >= 3: + # 三张以上图片网格布局 + + + # 第一行,三张 + if len(images)==3: + img_size = Cm(12) + else: + img_size = Cm(8) + gap = Cm(1) + for i in range(min(3, len(images))): + left = Cm(3) + (img_size + gap) * i + top = Cm(5 if subtitle_text else 4)+Cm(0.5) + pic = slide.shapes.add_picture(images[i]["path"], left, top, img_size, img_size) + + if "caption" in images[i]: + left_cap = left + top_cap = top + img_size + Cm(0.5) + width_cap = img_size + height_cap = Cm(1) + cap = slide.shapes.add_textbox(left_cap, top_cap, width_cap, height_cap) + tf = cap.text_frame + p = tf.add_paragraph() + if images[i]["caption"]: + p.text = images[i]["caption"] + else: + p.text = f"图片{i+1}" + p.font.size = Pt(10) + p.font.color.rgb = RGBColor(200, 230, 255) + p.alignment = PP_ALIGN.CENTER + + # 第二行(如果有4张以上图片) + img_size = Cm(4) + gap = Cm(1) + for i in range(3, len(images)): + left = Cm(3) + (img_size + gap) * (i - 3) + top = Cm(16 if subtitle_text else 15) + pic = slide.shapes.add_picture(images[i]["path"], left, top, img_size, img_size) + + if "caption" in images[i] and images[i]["caption"]: + left_cap = left + top_cap = top + img_size + Cm(0.5) + width_cap = img_size + height_cap = Cm(1) + cap = slide.shapes.add_textbox(left_cap, top_cap, width_cap, height_cap) + tf = cap.text_frame + p = tf.add_paragraph() + p.text = images[i]["caption"] + p.font.size = Pt(10) + p.font.color.rgb = RGBColor(200, 230, 255) + p.alignment = PP_ALIGN.CENTER + + return slide + +# ===== 封面幻灯片 ===== +def create_main_slide(prs, title_text, subject_text,content=None): + # ===== 封面幻灯片 ===== + slide_layout = prs.slide_layouts[6] # 空白布局 + slide = prs.slides.add_slide(slide_layout) + apply_gradient_background(slide) + + #添加宽屏主题 + left = Cm(2) + top = Cm(8) + width = Cm(36) + height = Cm(6) + subtitle_box = slide.shapes.add_textbox(left, top, width, height) + tf = subtitle_box.text_frame + tf.word_wrap = True + + p = tf.add_paragraph() + p.text = title_text + p.font.size = Pt(66) + p.font.color.rgb = RGBColor(200, 230, 255) + p.alignment = PP_ALIGN.LEFT + + # 添加副标题 + if subject_text: + left = Cm(1.5) + top = Cm(1.5) + width = Cm(36) + height = Cm(4) + title_box = slide.shapes.add_textbox(left, top, width, height) + tf = title_box.text_frame + tf.word_wrap = True + + p = tf.add_paragraph() + p.text = subject_text + p.font.size = Pt(36) + p.font.color.rgb = RGBColor(255, 255, 255) + p.font.bold = True + p.alignment = PP_ALIGN.LEFT + + # 添加其它标题 + if content: + i=0 + for line in content: + left = Cm(12) + top = Cm(12+i) + width = Cm(16) + height = Cm(2) + subtitle_box = slide.shapes.add_textbox(left, top, width, height) + tf = subtitle_box.text_frame + + p = tf.add_paragraph() + p.text = line["text"] + p.font.size = Pt(28) + p.font.color.rgb = RGBColor(200, 230, 255) + p.alignment = PP_ALIGN.CENTER + i+=3 +#end + +# ===== 结束幻灯片 ===== +def create_end_slide(prs,title,subtitle): + slide = create_content_slide( + prs, + title, + subtitle + ) + + img_width = Cm(8) + img_height = Cm(4) + print(prs.slide_width,prs.slide_width/Cm(1),prs.slide_width/Cm(1)/2-3) + left = Cm(prs.slide_width/Cm(1)/2-3) #屏幕中央 + top = Cm(8) + #添加logo + if gcfg["fs"]["logo"]!="": + pic = slide.shapes.add_picture(f'{gcfg["fs"]["path"]}/img/{gcfg["fs"]["logo"]}', left, top, img_width, img_height) + else: + pic = slide.shapes.add_picture(f'ui/images/logo2.jpg', left, top, img_width, img_height) + + # 在内容区域添加slogan和网址 + left = Cm(12) + top = Cm(12) + width = Cm(16) + height = Cm(3) + contact_box = slide.shapes.add_textbox(left, top, width, height) + tf = contact_box.text_frame + + p = tf.add_paragraph() + p.text = gcfg["fs"]["slogan"] + p.font.size = Pt(28) + p.font.color.rgb = RGBColor(255, 255, 255) + p.font.bold = True + p.alignment = PP_ALIGN.CENTER + p.space_after = Pt(16) + + p = tf.add_paragraph() + p.text = gcfg["fs"]["url"] + p.font.size = Pt(18) + p.font.color.rgb = RGBColor(200, 230, 255) + p.alignment = PP_ALIGN.CENTER + p.space_before = Pt(10) + + +#入口函数,根据html创建ppt +def create_unified_ppt(html,output_filename): + # 创建一个16:9宽屏演示文稿对象 + prs = Presentation() + #prs.slide_width = Inches(13.333) # 16:9的宽度 + #prs.slide_height = Inches(7.5) # 16:9的高度 + + prs.slide_width = Inches(16) # 16:9的宽度 + prs.slide_height = Inches(9) # 16:9的高度 + + ppt = parse_html_to_ppt(html) + + for slide in ppt: + if slide["type"]=="main": + create_main_slide(prs,slide["title"],slide["subtitle"],slide["content"]) + elif slide["type"]=="text": + create_content_slide( + prs,slide["title"],slide["subtitle"],slide["content"] + ) + elif slide["type"]=="image": + create_image_layout_slide( + prs,slide["title"],slide["subtitle"],slide["images"],slide["content"] + ) + elif slide["type"]=="chart": + create_chart_slide( + prs,slide["title"],slide["subtitle"],slide["chart_type"],slide["data"] + ) + elif slide["type"]=="table": + create_table_slide( + prs,slide["title"],slide["subtitle"],slide["header"],slide["data"],slide["content"] + ) + else: + pass + + create_end_slide(prs,"感谢聆听","欢迎指正") + # 保存演示文稿 + prs.save(output_filename) + +if __name__ == "__main__": + html_string =""" +

K3GPT数据分析智能体

+

三国工资表.xls

+

问题

按如下分析三国详情:1不同集团的人数,2不同集团不同级别的人数,3不同集团的平均服务年龄,4不同集团工种帝王人数

+

数据分析

集团分布

[chart][bar]

{\"categories\":[\"魏\",\"吴\",\"无\",\"蜀\"],\"count\":[3,3,3,2]}
+

集团->职级的分布

[chart][bar]

{\"L4\":[10,9,7,1],\"L3\":[8,0,4,1],\"L5\":[6,9,6,3],\"L6\":[4,5,3,2],\"L7\":[3,4,3,2],\"L8\":[2,2,2,0],\"L9\":[1,1,1,0],\"L2\":[0,0,1,0],\"categories\":[\"吴\",\"魏\",\"蜀\",\"无\"]}
+

不同集团对应的司龄(年)的常见六指标分析

[chart][bar]

{\"categories\":[\"蜀\",\"魏\",\"无\",\"吴\"],\"总和\":[397,586,179,560],\"平均值\":[14.703703703703704,19.533333333333335,19.88888888888889,16.470588235294116],\"中位数\":[12,19,20,15],\"最小值\":[5,10,8,5],\"最大值\":[40,35,30,40],\"个数\":[27,30,9,34]}
+

表格1

序号姓名集团工种职级工资(万)司龄(年)
1曹操帝王L91030
2刘备帝王L99.825
3孙权帝王L99.540
53孙策帝王L8810
54孙坚帝王L7715
55刘禅帝王L7640
56曹丕帝王L88.515
57曹叡帝王L7710
97袁绍帝王L7620
98袁术帝王L6515
100刘表帝王L64.525
+

小结

根据分析结果,三国工资表的详细情况如下:

  1. 集团人数分布 吴国:34人 魏国:30人 蜀国:27人 无集团:9人
  2. 集团-职级人数分布 吴国主要职级为L4(10人)、L3(8人)、L5(6人) 魏国最高职级达L9(1人),L5(9人)占比较大 蜀国职级分布较均衡,L4(7人)、L5(6人)为主
  3. 平均服务年龄 魏国平均19.53年(最高) 吴国16.47年 蜀国14.70年(最低) 无集团19.89年(可能包含临时人员)
  4. 帝王工种分布 吴国、魏国、无集团各有3名帝王,蜀国2名 帝王多集中在高级职级(L6-L9)

分析点评:  

", + + """ + + + # 使用前请替换示例图片路径为实际图片路径 + create_unified_ppt(html_string,"test.pptx") + print("带生成完成!")