Files
k3GPT/main/make_poster.py
2025-11-19 19:42:44 +08:00

1378 lines
45 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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')