diff --git a/main/make_poster.py b/main/make_poster.py new file mode 100644 index 0000000..102a497 --- /dev/null +++ b/main/make_poster.py @@ -0,0 +1,1378 @@ +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') \ No newline at end of file