1378 lines
45 KiB
Python
1378 lines
45 KiB
Python
|
|
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')
|