from PIL import Image, ImageDraw, ImageFont import os from parse_html import parse_html_to_ppt,parse_html_to_poster # 加载字体 - 使用系统路径 font_path_1 = "/usr/share/fonts/opentype/noto/NotoSansCJK-Thin.ttc" font_path_2 = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc" font_path_3 = "/usr/share/fonts/opentype/noto/NotoSansCJK-Black.ttc" font_path_4 = "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc" try: title_font = ImageFont.truetype(font_path_3, 100) # 粗体 subtitle_font = ImageFont.truetype(font_path_1, 60) # 细 end_font = ImageFont.truetype(font_path_1, 50) # 细 section_font = ImageFont.truetype(font_path_2, 100) # 粗体 section_title_font = ImageFont.truetype(font_path_4, 40) # 粗体 section_subtitle_font = ImageFont.truetype(font_path_4, 60) # 粗体 content_bold_font = ImageFont.truetype(font_path_4, 35) # 粗体 keyword_font = ImageFont.truetype(font_path_2, 40) # 中等 content_font = ImageFont.truetype(font_path_2, 35) # 中等 footer_font = ImageFont.truetype(font_path_1, 35) # 常规 except: raise Exception("缺少思源黑体字体文件") #颜色组 card_bg_color = None #颜色转换 def hex_to_rgb(hex_color): # 去掉可能的 '#' 字符 hex_color = hex_color.lstrip('#') # 使用 int 和 base=16 将十六进制字符串转换为整数,然后用 ',' 分隔这些值,并创建一个元组 rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) return rgb #获取相近色 def get_similar_hsv_color(rgb, dh=0.05, ds=0.1, dv=0.1): import colorsys r,g,b = rgb h, s, v = colorsys.rgb_to_hsv(r/255., g/255., b/255.) # 调整色调、饱和度和亮度 h = (h + dh) % 1 # 循环 s = max(0, min(1, s + ds)) v = max(0, min(1, v + dv)) r_new, g_new, b_new = [int(x*255) for x in colorsys.hsv_to_rgb(h, s, v)] return (r_new, g_new, b_new) import re def is_english_alpha(s): return bool(re.fullmatch(r'[A-Za-z]+', s)) #处理多行文本 def draw_multiline_text(draw, text, position, max_width, font, fill, line_spacing=10, only_calc=False): """ 在指定区域内绘制自动换行的文本 参数: draw - ImageDraw对象 text - 要绘制的文本 position - 起始位置 (x, y) max_width - 最大行宽 font - 字体对象 fill - 文本颜色 line_spacing - 行间距 (默认10) """ x, y = position lines = [] # 如果文本包含换行符,先按换行符分割 paragraphs = text.split('\n') for paragraph in paragraphs: words = [] # 中英文混合处理:将每个字符视为独立的"词" for char in paragraph: if char !='\n' and char !='\r' and char !="\xa0" and not char.isspace(): words.append(char) # # 添加空格字符作为可能的断点 # if char.isspace(): # words.append('') space_width = draw.textlength(' ', font=font) current_line = [] current_width = 0 for i,word in enumerate(words): word_width = draw.textlength(word, font=font) #中文超宽符号的处理 if word in ['“','”','《','》','『','』','·']: #print("中文",word,word_width) word_width += word_width elif word.islower() or word.isdigit(): #小写字母和数字要窄一点 #print("字母数字",word,word_width) word_width -= word_width//5 elif word=="-": word_width = word_width//2 # 如果当前行宽度加上新词宽度超过最大宽度 if int(current_width) + int(word_width) > int(max_width) and current_line: #print(''.join(current_line),int(current_width),int(word_width),int(max_width),word) #如果只剩下最后一个字,就放置在末尾 if len(words) -i ==1: current_line.append(word) break # 将当前行添加到行列表 lines.append(''.join(current_line)) #下一行计数 current_line = [word] current_width = word_width else: current_line.append(word) current_width += word_width # 添加最后一行 if current_line: lines.append(''.join(current_line)) # 计算行高 _, _, _, line_height = font.getbbox("A") # 绘制每一行文本 for line in lines: if not only_calc: draw.text((x, y), line, fill=fill, font=font) y += line_height + line_spacing return y #绘制图片 def draw_image(path_data,image_width): # 如果有 data:image/...;base64, 前缀,需要去掉 if path_data.startswith('data:image'): base64_str = path_data.split(',', 1)[1] # 取逗号后面的部分 import base64 from io import BytesIO # 解码 Base64 为二进制 image_data = base64.b64decode(base64_str) # 用 BytesIO 包装成“文件对象” image_file = BytesIO(image_data) content_img = Image.open(image_file) else: content_img = Image.open(path_data) # 获取原始尺寸 original_width, original_height = content_img.size # 计算高度的比例 width_ratio = image_width / original_width target_height = int(original_height * width_ratio) # 调整图像大小 resized_img = content_img.resize((image_width, target_height), Image.Resampling.LANCZOS) return resized_img,target_height #绘制表格 def draw_table(draw,data,table_top,table_left,table_width,header_font,data_font,text_color,header_color,tbc): if data is None: return table_top cell_height = 65 # 默认单元格高度 headers=data[0] data=data[1:] #去掉第一行的空行 rows = len(data) cols = len(headers) #table_height = cell_height * rows cell_width = table_width // cols # 绘制表头圆角矩形背景 draw.rounded_rectangle( (table_left-10, table_top, table_left+table_width+10, table_top+cell_height+15), radius=40, fill=header_color, # 深蓝色背景 outline=header_color ) # # 绘制表头分割线 for i in range(1, cols): x = table_left + i * cell_width # 绘制垂直线条,稍微短一点以避开圆角 draw.line( [(x, table_top + 10), (x, table_top + cell_height)], fill=(255, 255, 255, 180), # 半透明白色 width=2 ) for col in range(cols): text = headers[col] # 使用textbbox计算文本尺寸 text_bbox = draw.textbbox((0, 0), text, font=header_font) text_width = text_bbox[2] - text_bbox[0] text_height = text_bbox[3] - text_bbox[1] x = table_left + col * cell_width + (cell_width - text_width) // 2 y = table_top + (cell_height - text_height) // 2 draw.text((x, y), text, fill=(255, 255, 230), font=header_font) #微调一下数据表格的大小和位置,下移15 table_top +=15 table_width = table_width - 40 table_left +=20 cell_width = table_width // cols # 绘制数据行的起始位置 y = table_top + cell_height for row in range(rows): #计算最大行高 max_y = y + cell_height for col in range(cols): text = data[row][col] y1 = draw_multiline_text(draw, text, (table_left,y), cell_width-20, data_font, text_color, line_spacing=10, only_calc=True) if y1>max_y: max_y=y1 row_height = max_y - y for col in range(cols): # 计算单元格位置 x1 = table_left + col * cell_width #y1 = table_top + (row + 1) * cell_height y1 = y x2 = x1 + cell_width y2 = y1 + row_height # 交替行颜色 if row % 2 == 0: #bg_color = (248, 250, 253) # 浅色行 bg_color = (tbc[0],tbc[1],tbc[2],100) else: #bg_color = (240, 245, 251) # 更浅的蓝色行 bg_color = (tbc[0],tbc[1],tbc[2],200) # 绘制单元格背景 draw.rectangle([x1, y1, x2, y2], fill=bg_color) # 添加数据 text = data[row][col] draw_multiline_text(draw, text, (x1+15,y+10), cell_width-15, data_font, text_color, line_spacing=10, only_calc=False) # # 使用textbbox计算文本尺寸 # text_bbox = draw.textbbox((0, 0), text, font=data_font) # text_width = text_bbox[2] - text_bbox[0] # text_height = text_bbox[3] - text_bbox[1] # x = x1 + 15 # # 默认y位置 # y = y1 + (cell_height - text_height) // 2 # draw.text((x, y), text, fill=text_color, font=data_font) #一行的横线 y = max_y+10 draw.line( [(table_left, y), (table_left+table_width, y)], fill=(225, 232, 243), width=1 ) y +=1 #表格高度 table_height = y - table_top # 固定高度的横线 # for i in range(1, rows): # y = table_top + i * cell_height # draw.line( # [(table_left, y), (table_left+table_width, y)], # fill=(225, 232, 243), # width=1 # ) #竖线 for i in range(1, cols): x = table_left + i * cell_width draw.line( [(x, table_top + cell_height), (x, table_top+table_height)], fill=(225, 232, 243), width=1 ) #表格边线,三条,, # 深蓝色背景 draw.line( [(table_left, table_top + cell_height-5), (table_left, table_top+table_height)], fill=header_color, width=2 ) draw.line( [(table_left+table_width, table_top + cell_height-5), (table_left+table_width, table_top+table_height)], fill=header_color, width=2 ) draw.line( [(table_left, table_top + table_height), (table_left+table_width, table_top+table_height)], fill=header_color, width=2 ) return table_top+table_height+20 # 辅助函数:获取文本尺寸 def get_text_size(draw,text, font): bbox = draw.textbbox((0, 0), text, font=font) return bbox[2] - bbox[0], bbox[3] - bbox[1] #绘制头部样式1: 大logo+自动居中布局的标题 def draw_header_1(img,draw,poster_data,width,title_color,subtitle_color): # 1. 顶部标题区域,高度 title = poster_data["主标"] subtitle = poster_data["副标"] #主标题和副标题的宽度 title_w, title_h = get_text_size(draw,title, title_font) subtitle_w, subtitle_h = get_text_size(draw,subtitle, subtitle_font) # 复制logo logo_path = poster_data["logo_path"] # logo图片路径 logo = Image.open(logo_path) logo = logo.resize((400, 150), Image.Resampling.LANCZOS) # 调整logo大小 #标题很长,需要改变布局 if title_w > width-450 or subtitle_w > width-450: #logo和标题上下布局 # 这里位置是以左上角为起点的坐标(x, y) position = ((width - 400) // 2, 10) # 上面居中的位置 # 将logo复制到当前图像上 # 使用Image.paste方法,注意如果你的logo有透明度,需要使用mask参数 img.paste(logo, position, logo.convert('RGBA')) #主标 draw.text(((width - title_w) // 2, 180), title, fill=title_color, font=title_font) draw.text(((width - subtitle_w) // 2, 180 + title_h + 30), subtitle, fill=subtitle_color, font=subtitle_font) y = 180 + title_h + 30 + subtitle_h +60 else: #logo和标题左右布局 draw.text(((width - title_w) // 2+200, 50), title, fill=title_color, font=title_font) # 绘制副标题 subtitle_w, subtitle_h = get_text_size(draw,subtitle, subtitle_font) draw.text(((width - subtitle_w) // 2+200, 50 + title_h + 30), subtitle, fill=subtitle_color, font=subtitle_font) # 这里位置是以左上角为起点的坐标(x, y) position = (10, 10) # 左上角的位置 # 将logo复制到当前图像上 # 使用Image.paste方法,注意如果你的logo有透明度,需要使用mask参数 img.paste(logo, position, logo.convert('RGBA')) #固定值 y=300 # 装饰元素 - 金色圆点 for i in range(5): x = width * (0.1 + 0.2 * i) size = 10 if i % 2 == 0 else 15 draw.ellipse([(x, y), (x+size, y + size)], fill=title_color) return y #绘制头部样式2: 小logo+ 标题居左 def draw_header_2(img,draw,poster_data,width,title_color,subtitle_color): # 1. 顶部标题区域,高度 title = poster_data["主标"] subtitle = poster_data["副标"] #主标题和副标题的宽度 title_w, title_h = get_text_size(draw,title, title_font) subtitle_w, subtitle_h = get_text_size(draw,subtitle, subtitle_font) # 复制logo image_width = 250 #总宽度的4分之一 logo_path = poster_data["logo_path"] # logo图片路径 logo = Image.open(logo_path) # 获取原始尺寸 original_width, original_height = logo.size # 计算高度的比例 width_ratio = 250 / original_width target_height = int(original_height * width_ratio) # 调整图像大小 logo = logo.resize((image_width, target_height), Image.Resampling.LANCZOS) #logo和标题上下布局 # 这里位置是以左上角为起点的坐标(x, y) position = ((width - image_width)-50, 10) # 右边的位置 # 将logo复制到当前图像上 # 使用Image.paste方法,注意如果你的logo有透明度,需要使用mask参数 img.paste(logo, position, logo.convert('RGBA')) y = target_height +20 #主标 #draw.text((50, y), title, fill=title_color, font=title_font) y = draw_multiline_text( draw, title, (50, y), width-100, title_font, title_color, line_spacing=15, only_calc = False ) draw.text((50, y + 30), subtitle, fill=subtitle_color, font=subtitle_font) y += 30 + subtitle_h +60 draw.line( [(0, y), (width, y)], fill = title_color, width=1 ) return y #绘制头部样式2: 无logo+ 标题居左+ 日期 def draw_header_3(img,draw,poster_data,width,title_color,subtitle_color): # 1. 顶部标题区域,高度 title = poster_data["主标"] subtitle = poster_data["副标"] from datetime import date # 获取当前日期 current_date = date.today() # 如果需要将日期格式化为特定格式的字符串,可以使用 strftime 方法 formatted_date = current_date.strftime("%Y-%m-%d") print("格式化后的日期:", formatted_date) #主标题和副标题的宽度 date_w, date_h = get_text_size(draw,formatted_date, keyword_font) title_w, title_h = get_text_size(draw,title, title_font) subtitle_w, subtitle_h = get_text_size(draw,subtitle, subtitle_font) #日期 y = 20 #主标日期 draw.text(((width-date_w)-40, y), formatted_date, fill=subtitle_color, font=keyword_font) y = 100 #主标 #draw.text((50, y), title, fill=title_color, font=title_font) y = draw_multiline_text( draw, title, (50, y), width-100, title_font, title_color, line_spacing=15, only_calc = False ) # 绘制矩形,副标题题点缀 draw.rectangle([(50-10, y+15), (50+subtitle_w+10, y+subtitle_h+25)], fill=None, outline=subtitle_color) draw.text((50, y), subtitle, fill=subtitle_color, font=subtitle_font) y += subtitle_h +60 draw.line( [(0, y), (width, y)], fill = title_color, width=4 ) return y """ 数字卡片,卡片没有标题,有数字,通过后面增加一个区域做标题汇总 """ def draw_content_card_1(img,draw,poster_data,width,y_start,section_colors,card_bg_color,font_color): # 2. 内容区域 -多个内容块,流式布局按各自的内容大小来排 section_y_start = y_start y = section_y_start #内容的绝对值是 #内容 for i,slide in enumerate(poster_data["内容列表"]): # 计算位置,卡片的位置 y_pos = y #卡片宽度 card_width = width * 0.9 card_x = int((width - card_width) / 2) #y从标题颜色后面开始测算 y = y_pos + 50 #文字内容 if slide["content"]: y = draw_content_list(img,draw,slide,card_x,card_width,card_bg_color,font_color,section_colors,i,y,only_calc= True) # 卡片高度,保障一个最小值300的空间 if y - y_pos < 150: y += 100 #卡片高度 card_height = y - y_pos+30 # 创建圆角卡片 card = Image.new('RGBA', (int(card_width), int(card_height)), (0, 0, 0, 0)) card_draw = ImageDraw.Draw(card) # 圆角矩形填充 card_draw.rounded_rectangle([(0, 0), (card_width, card_height)], radius=40, fill=card_bg_color) # 抹去上面的圆角 bar_height = 40 card_draw.rectangle([(0, 0), (card_width, bar_height)], fill=card_bg_color) #添加色条 bar_height = 20 card_draw.rectangle([(0, 0), (card_width, bar_height)], fill=section_colors[i]) # 添加装饰元素 card_draw.ellipse([(card_width - 80, card_height - 80), (card_width - 20, card_height - 20)], fill=(255, 255, 255, 30)) # 粘贴卡片到海报 img.paste(card, (int(card_x), int(y_pos)), card) # 数字标识 num_text = str(i+1) num_w, num_h = get_text_size(draw,num_text, section_font) draw.text((card_x + 25, y_pos + 60), num_text, fill=section_colors[i], font=section_font) #真正绘制内容 y = y_pos + 50 #文字内容 if slide["content"]: y = draw_content_list(img,draw,slide,card_x,card_width,card_bg_color,font_color,section_colors,i,y,only_calc= False) # # 装饰元素 - 小圆点 # for j in range(3): # draw.ellipse([(card_x + card_width - 180 + j*40, y_pos + card_height/2 - 5), # (card_x + card_width - 160 + j*40, y_pos + card_height/2 + 15)], # fill=section_colors[i]) #下一个 y = y_pos + card_height+60 # 3. 底部关键字区域,和内容列表的主标对应 keywords = [content["主标"] for content in poster_data["内容列表"]] keyword_y = y+10 # 关键字容器 container_height = 180 container = Image.new('RGBA', (int(width*0.90), container_height), (0, 0, 0, 0)) container_draw = ImageDraw.Draw(container) container_draw.rounded_rectangle([(0, 0), (width*0.90, container_height)], radius=30, fill=card_bg_color) # 粘贴容器 img.paste(container, (int(width*0.05), int(keyword_y)), container) keyword_w = int(width*0.90) // len(keywords) # 绘制关键字 for idx, kw in enumerate(keywords): # 计算位置 x_pos = keyword_w //2 + keyword_w*idx + int(width*0.05) # 绘制图标背景 icon_size = 50 draw.ellipse([(x_pos - icon_size//2, keyword_y + 40), (x_pos + icon_size//2, keyword_y + 40 + icon_size)], fill=section_colors[idx]) # 绘制关键字文本 kw_w, kw_h = get_text_size(draw,kw, keyword_font) draw.text((x_pos - kw_w//2, keyword_y + 100), kw, fill=font_color, font=keyword_font) return y+180 """ 带标题卡片,直接在卡片前放置标题 """ def draw_content_card_2(img,draw,poster_data,width,y_start,section_colors,card_bg_color,font_color): # 2. 内容区域 -多个内容块,流式布局按各自的内容大小来排 section_y_start = y_start #卡片宽度 card_width = width * 0.9 card_x = int((width - card_width) / 2) #内容 y = section_y_start #内容的绝对值是 for i,slide in enumerate(poster_data["内容列表"]): # 计算位置,卡片的位置 y_pos = y #y从 主标 y = y_pos + 80 + 30 #文字内容 if slide["content"]: y = draw_content_list(img,draw,slide,card_x,card_width,card_bg_color,font_color,section_colors,i,y,only_calc= True) # 卡片高度,保障一个最小值300的空间 if y - y_pos < 150: y += 100 #卡片高度 card_height = y - y_pos # 绘制主标,标题 draw.rounded_rectangle([(card_x+card_width*0.055, y_pos), (card_x+card_width*0.95, y_pos+80)], radius=40, fill=section_colors[i]) num_w, num_h = get_text_size(draw,slide["主标"], section_title_font) draw.text((card_x + (card_width-num_w)//2, y_pos+5), slide["主标"], fill=(255,255,255), font=section_title_font) # 创建圆角卡片 card = Image.new('RGBA', (int(card_width), int(card_height)), (0, 0, 0, 0)) card_draw = ImageDraw.Draw(card) # 圆角矩形填充 card_draw.rounded_rectangle([(0, 0), (card_width, card_height)], radius=40, fill=card_bg_color) # 抹去上面的圆角 bar_height = 50 card_draw.rectangle([(0, 0), (card_width, bar_height)], fill=card_bg_color) # 添加装饰元素 card_draw.ellipse([(card_width - 80, card_height - 80), (card_width - 20, card_height - 20)], fill=(255, 255, 255, 30)) # 粘贴卡片到海报 img.paste(card, (int(card_x), int(y_pos+100)), card) #真正绘制内容 y = y_pos + 80 + 30 #文字内容 if slide["content"]: y = draw_content_list(img,draw,slide,card_x,card_width,card_bg_color,font_color,section_colors,i,y,only_calc= False) #下一个 y = y_pos +80+30+card_height+60 return y """ 彩色标签,直接在卡片前放置标题 """ def draw_content_card_5(img,draw,poster_data,width,y_start,section_colors,card_bg_color,font_color): # 2. 内容区域 -多个内容块,流式布局按各自的内容大小来排 section_y_start = y_start #卡片宽度 card_width = width * 0.9 card_x = int((width - card_width) / 2) #内容 y = section_y_start #内容的绝对值是 for i,slide in enumerate(poster_data["内容列表"]): # 计算位置,卡片的位置 y_pos = y #y从 主标开始,80是主标高度 y = y_pos +80 + 40 #文字内容 if slide["content"]: y = draw_content_list(img,draw,slide,card_x,card_width,card_bg_color,font_color,section_colors,i,y,only_calc= True) # 卡片高度,保障一个最小值300的空间 if y - y_pos < 150: y += 100 #卡片高度 card_height = y - y_pos -80 # 绘制主标,标题 # draw.rounded_rectangle([(card_x+card_width*0.055, y_pos), (card_x+card_width*0.95, y_pos+80)], # radius=40, fill=section_colors[i]) num_w, num_h = get_text_size(draw,slide["主标"], section_title_font) draw.text((card_x + (card_width-num_w)//2, y_pos+5), slide["主标"], fill=section_colors[i], font=section_title_font) # 创建圆角卡片 card = Image.new('RGBA', (int(card_width), int(card_height)), (0, 0, 0, 0)) card_draw = ImageDraw.Draw(card) # 圆角矩形填充 card_draw.rounded_rectangle([(0, 0), (card_width, card_height)], radius=40, fill=card_bg_color) #抹去左边的圆角 bar_width = 40 card_draw.rectangle([(0, 0), (bar_width, card_height)], fill=card_bg_color) # 左边的彩带 bar_width = 20 card_draw.rectangle([(0, 0), (bar_width, card_height)], fill=section_colors[i]) # 添加装饰元素 card_draw.ellipse([(card_width - 80, 10), (card_width - 20, 10+60)], #fill=(255, 255, 255, 30) fill=section_colors[i] ) # 粘贴卡片到海报 img.paste(card, (int(card_x), int(y_pos+100)), card) #真正绘制内容 y = y_pos +80 + 40 #文字内容 if slide["content"]: y = draw_content_list(img,draw,slide,card_x,card_width,card_bg_color,font_color,section_colors,i,y,only_calc= False) #下一个 y = y_pos +80+30+card_height+60 return y """ 小圆角卡片,标题放置在卡片内部 """ def draw_content_card_6(img,draw,poster_data,width,y_start,section_colors,card_bg_color,font_color): # 2. 内容区域 -多个内容块,流式布局按各自的内容大小来排 section_y_start = y_start #卡片宽度 card_width = width * 0.9 card_x = int((width - card_width) / 2) #内容 y = section_y_start #内容的绝对值是 for i,slide in enumerate(poster_data["内容列表"]): # 计算位置,卡片的位置 y_pos = y #y, 80是主标高度 y = y_pos +80 +30 #文字内容 (含副标题) if slide["content"]: y = draw_content_list(img,draw,slide,card_x,card_width,card_bg_color,font_color,section_colors,i,y,only_calc= True) # 卡片高度,保障一个最小值300的空间 if y - y_pos < 150: y += 100 #卡片高度 card_height = y - y_pos # 创建圆角卡片 card = Image.new('RGBA', (int(card_width), int(card_height)), (0, 0, 0, 0)) card_draw = ImageDraw.Draw(card) # 小圆角矩形填充 card_draw.rounded_rectangle([(0, 0), (card_width, card_height)], radius=15, fill=card_bg_color, outline= section_colors[i]) # 粘贴卡片到海报 img.paste(card, (int(card_x), int(y_pos)), card) #真正绘制内容 y = y_pos num_w, num_h = get_text_size(draw,slide["主标"], section_title_font) draw.rectangle([(card_x + 15, y+20+10), (card_x + 15+10, y+20+10+num_h)], fill= section_colors[i]) draw.text((card_x + 30, y+20), slide["主标"], fill=section_colors[i], font=section_title_font) y += num_h + 50 #文字内容(含副标题) if "content" in slide: y = draw_content_list(img,draw,slide,card_x,card_width,card_bg_color,font_color,section_colors,i,y,only_calc= False) #下一个 y = y_pos + card_height+60 return y #绘制内容列表 def draw_content_list(img,draw,slide,card_x,card_width,bg_color,font_color,section_colors,i,y,only_calc= True): ol=1 for j,content in enumerate(slide["content"]): #文字 if "text" in content: num_w = 40 if len(slide["content"]) ==1: num_w = 0 font = content_font if content["level"]==2: num_w = 0 font = content_bold_font if "bullet" in content:#二级标题 num_w= 15 if not only_calc: # draw.rectangle([(card_x +110, y+15), (card_x +110+20, y+15+20)], # fill=section_colors[i], # #outline=font_color, # #width=4, # ) # 圆形 # draw.ellipse([(card_x +110, y+15), (card_x +110+25, y+15+25)], # fill=font_color, # outline=section_colors[i], # width = 5, # ) triangle_points = [ (card_x +90+15, y+10), # 顶部顶点 (card_x +90, y+10+30), # 左下顶点 (card_x +90+30, y+10+30) # 右下顶点 ] draw.polygon(triangle_points, fill=section_colors[i], #outline=, #width = 5, ) if "square" in content: num_w= 30 if not only_calc: # 方形填充 draw.rectangle([(card_x +110, y+15), (card_x +110+20, y+15+20)], fill=section_colors[i], #outline=font_color, #width=4, ) #顺序号 if "number" in content: num_w= 50 if not only_calc: # 圆点填充 draw.ellipse([(card_x +110, y+5), (card_x +110+40, y+5+40)], fill=section_colors[i], #outline=font_color ) #数字 draw.text((card_x +110+10, y-2), str(ol), fill=card_bg_color, font=content_font) ol +=1 if content["level"]==1: #副标题 y = draw_multiline_text( draw, content["text"], (card_x +90, y), card_width-120, section_title_font, section_colors[i], line_spacing=15, only_calc = only_calc ) else: y = draw_multiline_text( draw, content["text"], (card_x +110+num_w, y), card_width-120-num_w, font, font_color, line_spacing=15, only_calc = only_calc ) y +=20 elif "image" in content: image = content image_width= int(card_width)-100 resized_img,target_height= draw_image(image["path"],image_width) draw.text((card_x + 100, y), image["caption"], fill=font_color, font=footer_font) img.paste(resized_img, (card_x+50,y+50), resized_img.convert('RGBA')) y += target_height+65 elif "table" in content: y = draw_table(draw,content["data"], y,card_x+40,card_width-80, keyword_font,content_font, font_color,section_colors[i], bg_color ) y +=20 return y """ 无卡片布局 """ def draw_content_3(img,draw,poster_data,width,y_start,section_colors,title_color,bg_color,font_color): # 2. 内容区域 -多个内容块,流式布局按各自的内容大小来排 section_y_start = y_start #卡片宽度 card_width = width * 0.9 card_x = int((width - card_width) / 2) #内容 y = section_y_start #内容的绝对值是 for i,slide in enumerate(poster_data["内容列表"]): # 计算位置,卡片的位置 y_pos = y #y从 主标 y = y_pos + 80 + 30 #内容 if slide["content"]: y = draw_content_list(img,draw,slide,card_x,card_width,bg_color,font_color,section_colors,i,y,only_calc= True) # 卡片高度,保障一个最小值300的空间 if y - y_pos < 150: y += 100 #卡片高度 card_height = y - y_pos # 绘制主标,标题点缀 draw.rectangle([(card_x, y_pos), (card_x+10, y_pos+70)], fill=section_colors[i]) num_w, num_h = get_text_size(draw,slide["主标"], section_title_font) draw.text((card_x+20, y_pos+5), slide["主标"], fill=title_color, font=section_title_font) #真正绘制内容 y = y_pos + 80 + 30 #文字内容 if slide["content"]: y = draw_content_list(img,draw,slide,card_x,card_width,bg_color,font_color,section_colors,i,y,only_calc= False) #下一个 y = y_pos +80+30+card_height+20 return y """ 无卡片,网格布局 """ def draw_content_4(img,draw,poster_data,width,y_start,section_colors,title_color,bg_color,font_color): # 2. 内容区域 -多个内容块,流式布局按各自的内容大小来排 section_y_start = y_start #卡片宽度 card_width = width * 0.9 card_x = int((width - card_width) / 2) #内容 y = section_y_start #内容的绝对值是 for i,slide in enumerate(poster_data["内容列表"]): # 计算位置,卡片的位置 y_pos = y #y从 主标 y = y_pos + 80 + 30 #文字内容 if slide["content"]: y = draw_content_list(img,draw,slide,card_x,card_width,bg_color,font_color,section_colors,i,y,only_calc= True) # 卡片高度,保障一个最小值300的空间 if y - y_pos < 150: y += 100 #卡片高度 card_height = y - y_pos #主标题 num_w, num_h = get_text_size(draw,slide["主标"], section_title_font) draw.text((card_x+20, y_pos+5), slide["主标"], fill=section_colors[i], font=section_title_font) # 绘制主标,标题下方点缀 draw.rectangle([(card_x+20, y_pos+30+num_h), (card_x+20+num_w, y_pos+30+num_h+10)], fill=section_colors[i]) #真正绘制内容 y = y_pos + 80 + 30 #文字内容 if slide["content"]: y = draw_content_list(img,draw,slide,card_x,card_width,bg_color,font_color,section_colors,i,y,only_calc= False) #下一个 y = y_pos +80+30+card_height+20 return y #总结的card def draw_submit_card(img,draw,poster_data,width,y,title_color,card_bg_color,subtitle_color,style="round"): card_width = int(width*0.90) card_x = int(width*0.05) container_height = draw_multiline_text( draw, poster_data["结语"], (card_x+40, y), card_width-40, end_font, title_color, line_spacing=15, only_calc=True )-y+20 container = Image.new('RGBA', (card_width, container_height), (0, 0, 0, 0)) container_draw = ImageDraw.Draw(container) if style=="round": container_draw.rounded_rectangle([(0, 0), (card_width, container_height)], radius=20, fill=card_bg_color) elif style=="rect": container_draw.rectangle([(0, 0), (card_width, container_height)], fill=card_bg_color) # 粘贴容器 img.paste(container, (card_x, y), container) y +=10 y = draw_multiline_text( draw, poster_data["结语"], (card_x+40, y), card_width-40, end_font, subtitle_color, line_spacing=15 ) return y #尾部样式1: 渐变圆点 def draw_footer_1(img,draw,poster_data,width,y,title_color,subtitle_color): # 装饰元素 - 渐变圆点 footer_dots_y = y #手机一整屏 if footer_dots_y <1980:#内容少的直接到底部 footer_dots_y = 1980 #13个点,一个点大,一个点小 for i in range(13): x = width * (0.1 + 0.066 * i) size = 10 + i % 4 color = subtitle_color draw.ellipse([(x, footer_dots_y), (x+size, footer_dots_y+size)], fill=color) #装饰元素 -线 footer_line_y = footer_dots_y+20 draw.line([(width*0.1, footer_line_y), (width*0.9, footer_line_y)], fill=title_color, width=2) #海报尾部文字 y = footer_line_y+30 for i,footer in enumerate(poster_data["报尾"]): if len(footer)<30: #单行,居中对齐 footer = footer.replace("."," · ") footer_w, footer_h = get_text_size(draw,footer, footer_font) draw.text(((width - footer_w) // 2, y), footer, fill=subtitle_color, font=footer_font) y += footer_h+10 else: #多行,居左对齐 y = draw_multiline_text( draw, footer, (40, y), width-40, footer_font, subtitle_color, line_spacing=15 ) #海报尾部图片 image_width = width //2 #海报的一半 y = y+30 for i,image in enumerate(poster_data["报尾图片"],1): # 调整图像大小 resized_img,target_height =draw_image(image["path"],image_width) #图片文字 if image["caption"] and image["caption"][-3:] not in ["png","jpg","peg"]: draw.text(((width-image_width)//2+10, y+i*10), image["caption"], fill=subtitle_color, font=footer_font) y += i*10+60 #图片居中 img.paste(resized_img, ((width-image_width)//2,y), resized_img.convert('RGBA')) y += target_height+10 return y+60 #手机适合的宣传图 def create_modern_mobile_poster(poster_data,poster_style={}): # 海报尺寸 (手机竖屏9:16比例) width, height = 1080, 20000 accent_color = (100, 180, 180) # 青蓝色点缀 # 主色调:深蓝背景 + 金色点缀 bg_color = (15, 30, 64) # 深蓝背景 title_color = (255, 215, 0) # 金色标题 subtitle_color = (220, 220, 240) #亚白 card_bg_color = (30, 50, 80, 220) #浅蓝 section_colors = [ (255,77,90), #粉红 (70, 130, 180), # 蓝色 (100, 180, 160), # 青色 (180, 120, 70), # 橙色 (184, 66, 55), # 砖红 ] font_color = (240, 240, 240) #接近白色 if poster_style: try: bg_color = hex_to_rgb(poster_style["bg_color"]) title_color = hex_to_rgb(poster_style["title_color"]) subtitle_color = hex_to_rgb(poster_style["subtitle_color"]) card_bg_color = hex_to_rgb(poster_style["card_bg_color"]) section_colors = [ hex_to_rgb(color) for color in poster_style["section_colors"]]*5 font_color = hex_to_rgb(poster_style["font_color"]) except: pass # 创建画布 img = Image.new('RGBA', (width, height),bg_color) draw = ImageDraw.Draw(img) if poster_style["style"]==4: #网格背景 size=50 bg_color1 = get_similar_hsv_color(bg_color) #横线 for i in range(0,height//size): y = size//2 + i * size draw.line( [(0, y), (width, y)], #fill=(int(0.299*subtitle_color[0]),int(0.587*subtitle_color[1]),int(0.114*subtitle_color[2])), fill = bg_color1, width=1 ) #竖线 for i in range(0, width//size): x = size//2 + i * size draw.line( [(x, 0), (x, height)], #fill=(subtitle_color[0],subtitle_color[1],subtitle_color[2],0), fill = bg_color1, width=1 ) # 1. 绘制头部 if poster_style["header_style"]==2: y = draw_header_2(img,draw,poster_data,width,title_color,subtitle_color) elif poster_style["header_style"]==3: y = draw_header_3(img,draw,poster_data,width,title_color,subtitle_color) else: y = draw_header_1(img,draw,poster_data,width,title_color,subtitle_color) # 2. 绘制中间内容 if poster_style["style"] ==1: #多彩卡片 # 2. 绘制内容 y +=60 y = draw_content_card_1(img,draw,poster_data,width,y,section_colors,card_bg_color,font_color) # 3.结语 y += 50 if poster_data["结语"]: y = draw_submit_card(img,draw,poster_data,width,y,title_color,card_bg_color,subtitle_color) elif poster_style["style"] ==2: #标题卡片 # 2.结语 y += 40 if poster_data["结语"]: y = draw_submit_card(img,draw,poster_data,width,y,title_color,card_bg_color,subtitle_color) # 3. 绘制内容 y +=50 y = draw_content_card_2(img,draw,poster_data,width,y,section_colors,card_bg_color,font_color) elif poster_style["style"] ==3: #无卡片 # 2.结语 if poster_data["结语"]: y += 40 y = draw_submit_card(img,draw,poster_data,width,y,title_color,card_bg_color,subtitle_color,"rect") # 3. 绘制内容 y +=50 y = draw_content_3(img,draw,poster_data,width,y,section_colors,title_color,bg_color,font_color) elif poster_style["style"] ==4: # 网格背景 # 2.结语 if poster_data["结语"]: y += 40 y = draw_submit_card(img,draw,poster_data,width,y,title_color,card_bg_color,subtitle_color,"rect") # 3. 绘制内容 y +=50 y = draw_content_4(img,draw,poster_data,width,y,section_colors,title_color,bg_color,font_color) elif poster_style["style"] ==5: #彩色标签卡片 # 2.绘制内容 y += 40 y = draw_content_card_5(img,draw,poster_data,width,y,section_colors,card_bg_color,font_color) # 3. 结语 y +=50 if poster_data["结语"]: y = draw_submit_card(img,draw,poster_data,width,y,title_color,card_bg_color,subtitle_color) elif poster_style["style"] ==6: #小圆角卡片 # 2. 结语 if poster_data["结语"]: y += 40 y = draw_submit_card(img,draw,poster_data,width,y,title_color,card_bg_color,subtitle_color) # 3. 绘制内容 y +=50 y = draw_content_card_6(img,draw,poster_data,width,y,section_colors,card_bg_color,font_color) # 4. 底部装饰、版权信息 y +=60 if poster_data["报尾"]: y = draw_footer_1(img,draw,poster_data,width,y,title_color,subtitle_color) #裁切图片 if y < height: new_img = Image.new('RGB', (width, y), bg_color) new_img.paste(img, (0, 0)) return new_img else: return img #入口函数,创建海报 def create_poster(content,path): #百科内容转化为结构 poster_data = parse_html_to_poster(content) from fun_calls import gen_poster_color #生成主题颜色 try: poster_style = gen_poster_color(poster_data) except Exception as e: print("主题生成出错",e) poster_style = { "style": 6, "header_style": 3, "bg_color": "#F0F0F0", "title_color": "#003366", "subtitle_color": "#FFA500", "font_color": "#FFFFFF", "card_bg_color": "#D3D3D3", "section_colors": ["#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF"] } poster_style["style"] = 6 poster_style["header_style"] = 3 # #测试使用 # poster_style={ # "style": 6, # "header_style":3, # "bg_color": "#FFFFFF", # "title_color": "#333333", # "subtitle_color": "#003366", # "font_color": "#000000", # "card_bg_color": "#F0F8FF", # "section_colors": ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEEAD"], # # "bg_color": "#000080", # # "title_color": "#FFFFFF", # # "subtitle_color": "#CCCCCC", # # "font_color": "#333333", # # "card_bg_color": "#ADD8E6", # # "section_colors": ["#FFFFFF", "#ADD8E6", "#87CEEB", "#B0C4DE", "#D3D3D3"] # } # 生成并保存海报 poster = create_modern_mobile_poster(poster_data,poster_style) poster.save(path) print('海报已生成: modern_poster.png') #入口函数,创建海报 def create_poster_style(content,path,poster_style): #百科内容转化为结构 poster_data = parse_html_to_poster(content) # 生成并保存海报 poster = create_modern_mobile_poster(poster_data,poster_style) poster.save(path) print('海报已生成: modern_poster.png')