Spaces:
Sleeping
Sleeping
from PIL import Image, ImageDraw, ImageFont | |
import re | |
import os | |
import io | |
from dataclasses import dataclass | |
import requests | |
from pypinyin import pinyin, Style | |
import rembg | |
import math | |
font_path_fangzhenghei = "src/assets/FangZhengHeiTiJianTi-1.ttf" | |
font_path_zihun59 = "src/assets/字魂59号-创粗黑.ttf" | |
font_path_notosans = "src/assets/NotoSansHans-Regular.otf" | |
font_path_feihuasong = "src/assets/FeiHuaSongTi-2.ttf" | |
font_path_wendingPLjianbaosong = "src/assets/gbsn00lp-2.ttf" | |
font_path_SourceHanSans = "src/assets/SourceHanSansCN-VF-2.otf" | |
font_path_notosansM = "src/assets/NotoSansHans-Medium.otf" | |
font_path_notosansB = "src/assets/NotoSansHans-Bold.otf" | |
font_path_notosansL = "src/assets/NotoSansHans-Light.otf" | |
font_path_pingfang = "src/assets/PingFang.ttc" | |
font_path_pingfangM = "src/assets/pingfang-M.ttf" | |
class text_info: | |
text: str | |
font_size: int | |
position: list | |
color: tuple = (0, 0, 0) | |
max_height: int = 1000 | |
max_width: int = 0 | |
text_width: int = 0 | |
text_height: int = 0 | |
line_space: int = 10 | |
font_path: str = None | |
em_font_path: str = None | |
def __post_init__(self): | |
self.wrap_text() | |
self.get_text_width() | |
self.get_text_height() | |
def wrap_text(self): | |
""" | |
resize to fix max_height and max_width | |
""" | |
words = re.findall(r"[a-zA-Z\d]+|[\u4e00-\u9fa5]|\n|.", self.text) | |
self.font = ImageFont.truetype(self.font_path, self.font_size) | |
lines = [] | |
current_line = "" | |
for word in words: | |
if word == "\n": | |
lines.append(current_line.strip()) | |
current_line = "" | |
continue | |
test_line = current_line + word | |
test_bbox = self.font.getbbox(test_line, anchor="la") | |
test_width = test_bbox[2] - test_bbox[0] # 计算宽度 | |
if test_width <= self.max_width and "\n" not in test_line: | |
current_line = test_line | |
else: | |
lines.append(current_line.strip()) | |
current_line = word | |
if current_line: | |
lines.append(current_line.strip()) | |
self.lines = lines | |
test_bbox = self.font.getbbox("\n".join(lines), anchor="la") | |
self.text_height = test_bbox[3] - test_bbox[1] | |
self.text_width = test_bbox[2] - test_bbox[0] # 计算宽度 | |
# print(lines, self.text_height, self.text_width) | |
key_lines = [line for line in lines if line.startswith("【】")] | |
if len(key_lines) > 0: | |
key_line = max(key_lines, key=len) | |
else: | |
key_line = False | |
if self.text_height > self.max_height or self.text_width > self.max_width or (key_line and not re.search("^" + re.escape(key_line) + "$", self.text, re.M)): | |
self.font_size -= 2 | |
self.wrap_text() | |
def get_text_width(self): | |
text_bbox = self.font.getbbox(self.text, anchor="la") | |
self.text_width = text_bbox[2] - text_bbox[0] | |
def get_text_height(self): | |
text_bbox = self.font.getbbox(self.text, anchor="la") | |
self.text_height = text_bbox[3] - text_bbox[1] | |
def write_text_on_image(img, text_info): | |
# 创建一个可以在给定图像上绘图的对象 | |
draw = ImageDraw.Draw(img) | |
# 加载字体,如果没有提供字体路径,则使用系统默认字体 | |
font = ImageFont.load_default().font_variant(size=text_info.font_size) | |
em_font = ImageFont.load_default().font_variant(size=text_info.font_size) | |
if text_info.font_path: | |
font = ImageFont.truetype(text_info.font_path, text_info.font_size) | |
if text_info.em_font_path: | |
em_font = ImageFont.truetype(text_info.em_font_path, text_info.font_size) | |
# 在图片上写入文字 | |
# draw.text(text_info.position, text_info.text, fill=text_info.color, font=font) | |
y_text = text_info.position[1] | |
wrapped_lines = text_info.lines | |
text_bbox_chinese = font.getbbox("中文行高") | |
for line in wrapped_lines: | |
text_bbox = font.getbbox(line) | |
line_width = text_bbox[2] - text_bbox[0] # 获取宽度 | |
line_height = max(text_bbox[3] - text_bbox[1], text_bbox_chinese[3] - text_bbox_chinese[1]) | |
# print(text_bbox[3] - text_bbox[1], text_bbox_chinese[3] - text_bbox_chinese[1], text_info.font_size) | |
if re.search(r"^【.*】.*?" + re.escape(line), text_info.text, re.M) or re.match(r"^【.*】.*?$", line): | |
draw.text((text_info.position[0], y_text), line.replace("【】", ""), fill=text_info.color, font=em_font) | |
else: | |
draw.text((text_info.position[0], y_text), line, fill=text_info.color, font=font) | |
# 更新y坐标以添加行间距并为下一行做准备 | |
y_text += line_height # + text_info.line_space | |
return img, y_text | |
def embed_image(background, foreground, offset_y=0, offset_x=None, standard_fg_size=None): | |
""" | |
粘贴图片 | |
Args: | |
background (_type_): 背景图片 | |
foreground (_type_): 前景图片 | |
offset_y (int, optional): 纵向偏移. Defaults to 1500. | |
standard_fg_size (tuple, optional): 前景缩放大小. Defaults to (1600, 450). | |
Returns: | |
_type_: _description_ | |
""" | |
# 获取背景图片的尺寸 | |
if standard_fg_size is None: | |
standard_fg_size = foreground.size | |
standard_fg_width, standard_fg_height = standard_fg_size | |
bg_width, bg_height = background.size | |
fg_width, fg_height = foreground.size | |
fg_resize_percent = max(fg_width / standard_fg_width, fg_height / standard_fg_height) | |
# print((fg_width / standard_fg_width, fg_height / standard_fg_height)) | |
foreground = foreground.resize((int(fg_width // fg_resize_percent), int(fg_height // fg_resize_percent))) | |
fg_width, fg_height = foreground.size | |
if offset_x is None: | |
offset_x = (bg_width - fg_width) // 2 # 横轴偏移使小图居中 | |
background.paste(foreground, (int(offset_x), int(offset_y)), foreground) | |
return background | |
img_path_dict = { | |
"old": "src/assets/InvitationTemplateCompressed.pdf", | |
"common": "src/assets/旧空白邀请函图片.jpg", | |
"观礼": "src/assets/带名字.jpg", | |
"演讲": "src/assets/带名字和会.jpg", | |
"致辞": "src/assets/带名字和会.jpg", | |
} | |
font_path = "src/assets/ZhouZiSongTi7000Zi-2.otf" | |
font_path = "src/assets/FangZhengHeiTiJianTi-1.ttf" | |
def writeInvitationPDF(person, debug=False): | |
"旧空白邀请函图片.jpg" | |
font_size = 40 | |
resized_name_font_size = font_size * 5 / len(person.name + person.title) | |
resized_conference_font_size = font_size * 9 / len(person.conference) | |
img_path = img_path_dict[person.category] | |
image = Image.open(img_path) | |
draw = ImageDraw.Draw(image) | |
text_color = (255, 255, 255) # 白色 RGB | |
draw.text( | |
(182, 150 + max(0, font_size - resized_name_font_size)), | |
person.name + person.title, | |
fill=text_color, | |
font=ImageFont.truetype(font_path, min(font_size, resized_name_font_size)), | |
) | |
draw.text( | |
(102, 532 + max(0, font_size - resized_conference_font_size)), | |
person.conference, | |
fill=text_color, | |
font=ImageFont.truetype(font_path, min(font_size, resized_conference_font_size)), | |
) | |
imgStream = io.BytesIO() | |
# image.save(imgStream) | |
# doc.save(pdfStream) | |
# doc.close() | |
if debug: | |
image.save("output/image_with_text.jpg") | |
return imgStream | |
def write_text( | |
text, font_path, image, H_PADDING_RATION, font_size, max_height=None, color=(255, 255, 255), em_font_path=None, alignment="left", | |
start_y=0, start_x=0, max_width=None, part_width=10000 | |
): | |
bg_width = image.size[0] | |
if max_width is None: | |
max_width = bg_width * (1 - 2 * H_PADDING_RATION) | |
if max_height is None: | |
max_height = font_size * len(text.split("\n")) | |
text = text_info( | |
text=text, | |
font_size=font_size, | |
position=[start_x, start_y], | |
color=color, | |
max_width=max_width, | |
max_height=max_height, | |
font_path=font_path, | |
em_font_path=em_font_path, | |
) | |
if alignment == "right": | |
text.position[0] = bg_width * (1 - H_PADDING_RATION) - text.text_width | |
elif alignment == "medium": | |
text.position[0] = (min(part_width, bg_width) - text.text_width) // 2 + start_x | |
elif alignment == "left": | |
text.position[0] = bg_width * H_PADDING_RATION | |
print(text) | |
image, next_height = write_text_on_image(image, text) | |
return text, image, next_height | |
def make_exhibitor_poster(logo_link, slogan, content): | |
background = Image.open("src/assets/展商海报白板.jpg") | |
H_PADDING_RATION = 0.08 | |
V_PADDING = 25 | |
V_START = 950 | |
MAX_HEIGHT = 520 | |
res = requests.get(logo_link) | |
foreground = Image.open(io.BytesIO(res.content)) | |
# foreground = Image.open(logo_path_list[FOREGROUND_ID]) | |
background = embed_image(background, foreground, offset_y=750, standard_fg_size=(1600 // 2, 450 // 2)) | |
text = slogan | |
font_size = 110 // 2 | |
font_path = font_path_fangzhenghei | |
slogan, background, next_height = write_text( | |
text, | |
font_path, | |
background, | |
H_PADDING_RATION, | |
font_size, | |
color=(0, 0, 0), | |
alignment="medium", | |
start_y=V_START, | |
) | |
content, background, next_height = write_text( | |
content, | |
font_path_SourceHanSans, | |
background, | |
H_PADDING_RATION, | |
100, | |
max_height=MAX_HEIGHT, | |
color=(0, 0, 0), | |
em_font_path=font_path_fangzhenghei, | |
alignment="left", | |
start_y=next_height + V_PADDING, | |
) | |
img_stream = io.BytesIO() | |
background.save(img_stream, format="JPEG") | |
img_stream.seek(0) | |
background.save("output/展商海报.jpg") | |
return img_stream | |
def cut_image_blank(image): | |
# 获取图片的宽度和高度 | |
width, height = image.size | |
# 初始化边界框 | |
left = width | |
top = height | |
right = 0 | |
bottom = 0 | |
# 遍历图片的每个像素,找到非透明的区域 | |
for x in range(width): | |
for y in range(height): | |
pixel = image.getpixel((x, y)) | |
if pixel[3] != 0: # 检查透明度通道 | |
if x < left: | |
left = x | |
if x > right: | |
right = x | |
if y < top: | |
top = y | |
if y > bottom: | |
bottom = y | |
if right < left or bottom < top: | |
return image | |
cropped_image = image.crop((left, top, right + 1, bottom + 1)) | |
return cropped_image | |
def rm_img_bg(image): | |
image = rembg.remove(image) | |
# image = cut_image_blank(image) | |
return image | |
def compose_guest_poster(name, name_pinyin, guest_photo, titles): | |
background = Image.open("src/assets/Guest04.jpg").convert("RGBA") | |
background = background.resize((1080 * 2, 2400 * 2)) | |
title_list = titles.split("\n") | |
# guest = Image.open("src/assets/Guest.png").convert("RGBA") | |
# higher_mask = Image.open("src/assets/Guest01.png").convert("RGBA") | |
H_PADDING_RATION = 0.08 | |
V_PADDING = 5 | |
V_START = 3020 | |
bg_width, bg_height = background.size | |
background = embed_image(background, guest_photo, offset_y=1300, standard_fg_size=(2160, 2600)) | |
lower_mask = Image.open("src/assets/Guest03.png").convert("RGBA") | |
background = embed_image(background, lower_mask, standard_fg_size=background.size) | |
higher_mask = Image.open("src/assets/Guest02.png").convert("RGBA") | |
background = embed_image(background, higher_mask, standard_fg_size=background.size) | |
seal = Image.open("src/assets/Guest01.png").convert("RGBA") | |
background = embed_image(background, seal, standard_fg_size=background.size) | |
# background = background.convert("RGB") | |
font_size = 210 | |
font_path = font_path_zihun59 | |
text = name | |
name, background, next_height = write_text( | |
text, | |
font_path, | |
background, | |
H_PADDING_RATION, | |
font_size, | |
alignment="right", | |
start_y=V_START, | |
) | |
next_height += 50 | |
font_size = 100 | |
font_path = font_path_notosans | |
if name_pinyin == "": | |
pinyin_list = pinyin(name.text, style=Style.NORMAL) | |
name_pinyin = (pinyin_list[0][0] + " " + "".join([item[0] for item in pinyin_list[1:]])).title() | |
text = name_pinyin | |
name_pinyin, background, next_height = write_text( | |
text, | |
font_path, | |
background, | |
H_PADDING_RATION, | |
font_size, | |
alignment="right", | |
start_y=next_height + V_PADDING, | |
) | |
next_height += 100 | |
font_size = 70 | |
font_path = font_path_notosans | |
for title in title_list: | |
text = title | |
title, background, next_height = write_text( | |
text, | |
font_path, | |
background, | |
H_PADDING_RATION, | |
font_size, | |
alignment="right", | |
start_y=next_height + V_PADDING, | |
) | |
background = background.convert("RGB") | |
img_stream = io.BytesIO() | |
background.save(img_stream, format="JPEG") | |
img_stream.seek(0) | |
if os.path.exists("src/assets/output"): | |
background.save("src/assets/output/嘉宾海报.jpg") | |
return img_stream | |
def make_guest_poster(name, name_pinyin, photo_link, original_photo_link, titles): | |
if photo_link != "": | |
res = requests.get(photo_link) | |
guest_photo = Image.open(io.BytesIO(res.content)).convert("RGBA") | |
elif original_photo_link != "": | |
res = requests.get(original_photo_link) | |
original_guest_photo = Image.open(io.BytesIO(res.content)) | |
guest_photo = rm_img_bg(original_guest_photo) | |
if os.path.exists("src/assets/output"): | |
guest_photo.save("src/assets/output/rmbg.png") | |
else: | |
return None | |
img_stream = compose_guest_poster(name, name_pinyin, guest_photo, titles) | |
return img_stream | |
def crop_round_img(image, circle_center, circle_radius): | |
mask = Image.new("L", image.size, 0) | |
draw = ImageDraw.Draw(mask) | |
draw.ellipse((circle_center[0] - circle_radius, circle_center[1] - circle_radius, circle_center[0] + circle_radius, circle_center[1] + circle_radius), fill=255) | |
output_image = Image.new("RGBA", image.size) | |
output_image.paste(image, (0, 0), mask) | |
output_image = output_image.crop( | |
(circle_center[0] - circle_radius, circle_center[1] - circle_radius, circle_center[0] + circle_radius, circle_center[1] + circle_radius) | |
) | |
# output_image = cut_image_blank(output_image) | |
if os.path.exists("src/assets/output"): | |
output_image.save("src/assets/output/cropped_circle_image.png") | |
return output_image | |
def make_conference_poster(conference_name, conference_name_eng, conference_name_full, conference_info, conference_rundown): | |
BACKGROUND_COLOR = (1, 6, 143) | |
background = Image.open("src/assets/schedule/top_background.jpg").convert("RGBA") | |
bar = Image.open("src/assets/schedule/bar.png").convert("RGBA") | |
point_bar = Image.open("src/assets/schedule/point_bar.png").convert("RGBA") | |
H_PADDING_RATION = 0.06 | |
V_PADDING = 80 | |
V_START = 1100 | |
bg_width, bg_height = background.size | |
print(background.size) | |
print(point_bar.size) | |
schedule_top, top_next_height = make_conference_schedule_top( | |
background, conference_name, conference_name_eng, conference_name_full, conference_info, bar, H_PADDING_RATION, V_START | |
) | |
schedule_components = [schedule_top] | |
########################################################## | |
for rundown in conference_rundown: | |
rundown_pic = write_rundown(BACKGROUND_COLOR, rundown, point_bar, H_PADDING_RATION, bg_width) | |
# if rundown["type"] == "主持人": | |
# schedule_components = schedule_components[:1] + [rundown_pic] + schedule_components[1:] | |
# else: | |
schedule_components.append(rundown_pic) | |
widths, heights = zip(*(i.size for i in schedule_components)) | |
schedule_poster = Image.new("RGB", (bg_width, sum(heights))) | |
x_offset = 0 | |
for img in schedule_components: | |
schedule_poster.paste(img, (0, x_offset)) | |
x_offset += img.size[1] | |
img_stream = io.BytesIO() | |
schedule_poster.save(img_stream, format="JPEG") | |
img_stream.seek(0) | |
if os.path.exists("src/assets/output"): | |
schedule_poster.save("src/assets/output/schedule.jpg") | |
return img_stream | |
def write_rundown(BACKGROUND_COLOR, rundown, point_bar, H_PADDING_RATION, bg_width): | |
background = Image.new("RGB", (bg_width, 6000), BACKGROUND_COLOR) | |
next_height = 0 | |
photo_size_ratio = 0.12 | |
V_PADDING = 80 | |
background = embed_image( | |
background, | |
point_bar, | |
offset_y = V_PADDING, | |
) | |
time_width = 0 | |
font_size = 130 | |
font_path = font_path_notosansM | |
# print(point_bar.size) | |
if rundown["type"] != "主持人": | |
text = rundown["start_time"][:-3] + "-" + rundown["end_time"][:-3] | |
text_ = text_info(text, font_size, [0, 0], | |
max_height=font_size, | |
max_width=bg_width * (1 - H_PADDING_RATION * 2.5), | |
font_path=font_path) | |
start_y = V_PADDING + (point_bar.height - text_.font_size) // 2 | |
conference_rundown_time, background, next_height = write_text( | |
text, font_path, background, H_PADDING_RATION * 1.5, font_size, | |
start_y=start_y, | |
alignment="left" | |
) | |
time_width = conference_rundown_time.text_width | |
rundown["title"] = rundown["title"] if "title" in rundown else "" | |
text = rundown["type"] # rundown["title"] if rundown["type"] != "主持人" else "主持人" | |
pioneer_part_width = H_PADDING_RATION * 1.5 * bg_width + time_width + font_size // 2 | |
text_ = text_info( | |
text, | |
font_size, | |
[0, 0], | |
max_height=font_size, | |
max_width=bg_width - pioneer_part_width - H_PADDING_RATION * bg_width, font_path=font_path) | |
start_y = V_PADDING + (point_bar.height - text_.font_size) // 2 | |
conference_rundown_type, background, next_height = write_text( | |
text, | |
font_path, | |
background, | |
pioneer_part_width / bg_width, | |
font_size, | |
start_y=start_y, | |
max_width=bg_width - pioneer_part_width - H_PADDING_RATION * bg_width, | |
alignment="left", | |
) | |
next_height = V_PADDING + point_bar.size[1] + V_PADDING | |
if rundown["type"] != "主持人": | |
text = rundown["title"] # rundown["title"] if rundown["type"] != "主持人" else "主持人" | |
pioneer_part_width = H_PADDING_RATION * 1.5 * bg_width + time_width + font_size // 2 | |
text_ = text_info( | |
text, | |
font_size, | |
[0, 0], | |
max_height=font_size, | |
max_width=bg_width - pioneer_part_width - H_PADDING_RATION * bg_width, font_path=font_path) | |
# start_y = V_PADDING + (point_bar.height - text_.font_size) // 2 | |
conference_rundown_type, background, next_height = write_text( | |
text, | |
font_path, | |
background, | |
pioneer_part_width / bg_width, | |
font_size, | |
start_y=next_height, | |
max_width=bg_width - 2 * H_PADDING_RATION * bg_width, | |
alignment="medium", | |
) | |
for person in rundown["people"]: | |
# if person["photo_link"] != "": | |
next_height += V_PADDING | |
background, next_height = write_person_line(background, person, H_PADDING_RATION, next_height, photo_size_ratio) | |
background = background.convert("RGB") | |
background = background.crop((0, 0, background.size[0], next_height + V_PADDING)) | |
if os.path.exists("src/assets/output"): | |
background.save("src/assets/output/schedule_rundown.jpg") | |
return background | |
def write_person_line(background, person, H_PADDING_RATION, start_y, photo_size_ratio, scale=1, text_start_padding=120, name_font_size=120, title_font_size=80): | |
bg_width = background.size[0] | |
text = person["name"] | |
font_size = name_font_size * scale | |
font_path = font_path_zihun59 | |
alignment = "left" | |
pioneer_part_width = H_PADDING_RATION * bg_width + text_start_padding * scale | |
radius = int(photo_size_ratio * bg_width) | |
if person["photo_link"] != "": | |
round_photo = write_round_photo(background, person, H_PADDING_RATION * bg_width + radius, start_y + radius, radius) | |
pioneer_part_width += round_photo.size[1] | |
titles = person["title"].split("\n") | |
name_padding = 50 * scale | |
title_padding = 20 * scale | |
people_text_y = start_y + (2 * radius - font_size - name_padding - title_font_size * scale * len(titles)) // 2 | |
conference_rundown_people_name, background, next_height = write_text( | |
text, | |
font_path, | |
background, | |
pioneer_part_width / bg_width, | |
font_size, | |
start_y=people_text_y, | |
max_width=bg_width - pioneer_part_width - H_PADDING_RATION * bg_width, | |
alignment=alignment | |
) | |
next_height += name_padding | |
font_size = title_font_size * scale | |
font_path = font_path_notosansM | |
# pioneer_part_width = H_PADDING_RATION * bg_width + round_photo.size[1] + font_size | |
for i, text in enumerate(titles): | |
conference_rundown_people_title, background, next_height = write_text( | |
text, | |
font_path, | |
background, | |
pioneer_part_width / bg_width, | |
font_size, | |
start_y=next_height + title_padding, | |
max_width=bg_width - pioneer_part_width - H_PADDING_RATION * bg_width, | |
max_height=radius * 1.2, | |
alignment=alignment, | |
) | |
return background, start_y + radius * 2 | |
def write_round_photo(background, person, start_x, start_y, radius): | |
photo = Image.new("RGBA", (100, 100), (125, 125, 125, 0)) | |
res = requests.get(person["photo_link"]) | |
photo = Image.open(io.BytesIO(res.content)).convert("RGBA") | |
width, height = photo.size | |
circle_center = (width // 2, height // 3.75) | |
circle_radius = min(width//2, height // 3.75) | |
print(width, height, circle_center, circle_radius) | |
round_photo = crop_round_img(photo, circle_center, circle_radius) | |
round_photo = round_photo.resize((radius*2, radius*2)) | |
background = embed_image(background, round_photo, offset_y=start_y-radius, offset_x=start_x-radius) | |
return round_photo | |
def make_conference_schedule_top(background, conference_name, conference_name_eng, conference_name_full, conference_info, bar, H_PADDING_RATION, V_START): | |
font_size = 250 | |
font_path = font_path_pingfangM | |
text = conference_name | |
conference_name, background, next_height = write_text( | |
text, | |
font_path, | |
background, | |
H_PADDING_RATION, | |
font_size, | |
start_y=V_START, | |
alignment="left", | |
) | |
font_size = 110 | |
font_path = font_path_notosans | |
text = conference_name_eng | |
V_PADDING = 120 | |
conference_name_eng, background, next_height = write_text( | |
text, font_path, background, H_PADDING_RATION, font_size, | |
start_y=next_height + V_PADDING, alignment="left" | |
) | |
font_size = 160 | |
font_path = font_path_notosansM | |
text = "——" + conference_name_full + "——" | |
V_PADDING = 180 | |
conference_name_full, background, next_height = write_text( | |
text, font_path, background, H_PADDING_RATION, font_size, start_y=next_height + V_PADDING, alignment="medium" | |
) | |
# write_text(" ", font_path, background, H_PADDING_RATION, 200, start_height=next_height + 400, alignment="medium") | |
INNER_PADDING = 60 | |
next_height = next_height + V_PADDING - INNER_PADDING | |
background, next_height = write_conference_info(conference_info, background, bar, H_PADDING_RATION, INNER_PADDING, next_height) | |
background = background.convert("RGB") | |
background = background.crop((0, 0, background.size[0], next_height + V_PADDING)) | |
if os.path.exists("src/assets/output"): | |
background.save("src/assets/output/schedule_top.jpg") | |
return background, next_height | |
def write_conference_info(conference_info, background, bar, H_PADDING_RATION, V_PADDING, next_height): | |
bg_width, vg_height = background.size | |
for i, info in enumerate(conference_info): | |
font_size = 110 | |
font_path = font_path_notosansB | |
text = info[0] | |
bar = bar.resize((int(bg_width * 0.06), int(font_size * 0.7))) | |
info_name, background, next_height = write_text( | |
text, font_path, background, H_PADDING_RATION, font_size, start_y=next_height + V_PADDING, alignment="left" | |
) | |
background = embed_image( | |
background, bar, offset_y=next_height - (bar.size[1] + info_name.font_size) // 2, offset_x=bg_width * H_PADDING_RATION + info_name.text_width | |
) | |
font_size = 80 | |
text = info[1] | |
pioneer_part_width = bar.size[0] + info_name.text_width + font_size | |
text_ = text_info( | |
text, | |
font_size, | |
[0, 0], | |
max_height=font_size, | |
max_width=bg_width * (1 - 2 * H_PADDING_RATION) - info_name.text_width - bar.width, | |
font_path=font_path) | |
info_content, background, _ = write_text( | |
text, | |
font_path, | |
background, | |
H_PADDING_RATION + pioneer_part_width / bg_width, | |
font_size, | |
max_width=bg_width * (1 - 2 * H_PADDING_RATION) - info_name.text_width - bar.width, | |
start_y=next_height - info_name.font_size + (info_name.font_size - text_.font_size) // 2, | |
alignment="left", | |
) | |
# next_height = next_height + info_name.font_size | |
return background, next_height | |
def getGuestComposePoster(conference_name, conference_eng_name, people): | |
top = Image.open("src/assets/GuestComposePoster/top.png").convert("RGBA") | |
card = Image.open("src/assets/GuestComposePoster/card.png").convert("RGBA") | |
V_PADDING = 80 | |
background = Image.new("RGB", (top.size[0], card.size[1] * math.ceil(len(people) / 3) + V_PADDING), (255, 255, 255)) | |
bg_width, bg_height = background.size | |
H_PADDING = (bg_width - 3*card.size[0]) / 2 | |
V_START = 1200 | |
print(top.size) | |
print(background.size) | |
font_size = 220 | |
font_path = font_path_notosansL | |
text = conference_name | |
conference_name, top, next_height = write_text( | |
text, | |
font_path, | |
top, | |
H_PADDING / bg_width, | |
font_size, | |
start_y=V_START, | |
alignment="medium", | |
) | |
font_size = 250 | |
font_path = font_path_notosansL | |
text = conference_eng_name | |
conference_eng_name, top, next_height = write_text( | |
text, | |
font_path, | |
top, | |
H_PADDING / bg_width, | |
font_size, | |
start_y=next_height + V_PADDING, | |
max_height=700, | |
alignment="medium", | |
) | |
people = [p for p in people if p["photo_link"] != ""] | |
for (i, person) in enumerate(people): | |
background = write_person_card(background, card, person, H_PADDING + card.size[0] * (i % 3), card.height * (i // 3)) | |
guest_compose_poster = Image.new("RGB", (bg_width, background.size[1] + top.size[1])) | |
guest_compose_poster.paste(top, (0, 0)) | |
guest_compose_poster.paste(background, (0, top.size[1])) | |
img_stream = io.BytesIO() | |
guest_compose_poster.save(img_stream, format="JPEG") | |
img_stream.seek(0) | |
if os.path.exists("src/assets/output"): | |
guest_compose_poster.save("src/assets/output/guest_compose_poster.jpg") | |
return img_stream | |
def write_person_card(background, card, person, H_PADDING, start_y): | |
bg_width = background.size[0] | |
# print(bg_width * photo_size_ratio) | |
white_bar = 70 | |
radius = int(0.67 * card.width) // 2 | |
round_photo = write_round_photo(background, person, H_PADDING + card.width // 2, start_y+white_bar+radius, radius) | |
background = embed_image(background, card, offset_y=start_y, offset_x=H_PADDING) | |
text = person["name"] | |
font_size = 120 | |
font_path = font_path_zihun59 | |
pioneer_part_width = H_PADDING | |
people_text_y = start_y + 2 * radius + white_bar + 20 | |
guest_compose_poster_people_name, background, next_height = write_text( | |
text, font_path, background, | |
pioneer_part_width / bg_width, | |
font_size, | |
start_y=people_text_y, | |
start_x=H_PADDING, | |
part_width=card.width, | |
max_width=card.width, | |
alignment="medium", | |
# color=(0,0,0) | |
) | |
text = "\n".join(person["title"].split("\n")[:5]) | |
font_size = 40 | |
font_path = font_path_notosansM | |
next_height += 30 | |
for i, text in enumerate(text.split("\n")): | |
pioneer_part_width = H_PADDING | |
people_text_y = next_height + 10 | |
guest_compose_poster_people_title, background, next_height = write_text( | |
text, font_path, background, | |
pioneer_part_width / bg_width, | |
font_size, | |
start_y=people_text_y, | |
start_x=H_PADDING, | |
part_width=card.width, | |
max_width=card.width-white_bar, | |
alignment="medium", | |
# color=(0,0,0) | |
) | |
return background | |
def getGuestEScreen(root_type, person): | |
template_info = { | |
"full": { | |
"width": 3328, | |
"height": 1152 | |
}, | |
"half": { | |
"width": 3072, | |
"height": 1280 | |
}, | |
"M": { | |
"width": 3072, | |
"height": 1280 | |
} | |
} | |
background = Image.open(f"src/assets/EScreen/{root_type}.jpg") | |
bg_width, bg_height = background.size | |
scale = bg_width / template_info[root_type]["width"] | |
radius = 600 // 2 * scale | |
radius_ratio = radius / bg_width | |
H_PADDING_RATION = 0.22 | |
V_START = bg_height // 2 - radius | |
print(background.size) | |
write_person_line(background, person, H_PADDING_RATION, V_START, radius_ratio, | |
text_start_padding=200, name_font_size=100, title_font_size=50, scale=scale) | |
img_stream = io.BytesIO() | |
background.save(img_stream, format="JPEG") | |
img_stream.seek(0) | |
if os.path.exists("src/assets/output"): | |
background.save("src/assets/output/hall_screen.jpg") | |
return img_stream | |
if __name__ == "__main__": | |
# image_path = 'src/assets/邀请函封面.jpg' # 替换为你的图片文件路径 | |
# text_to_write = "你好,世界!" | |
# font_path = "src/assets/FangZhengHeiTiJianTi-1.ttf" | |
# position = (120, 140) | |
from collections import namedtuple | |
Person = namedtuple("Person", ["name", "title", "category", "conference", "respond_person"]) | |
person = Person(name="一二三四五六", title="先生", category="演讲", conference="8月21日中欧智慧论坛,做主题演讲", respond_person="Eazy") | |
output_path = "output/output.pdf" | |
# writeInvitationPDF(person, debug=True) | |
text_list = [ | |
"""Shanghai 协砺科技有限公司专注于青少年科技教育12年,通过STEAM25教育模式培养青少年的科学思维和创新能力。在全国设有数十家学习中心,服务上万名学员,提供科创、机器人和编程课程。公司还设有赛事研究中心,由专业工程师团队提供赛事研究和策略支持。学员在各类科技赛事中屡获佳绩。协砺科技致力于推动中国科技教育的创新与发展,为国家繁荣贡献力量。 | |
【主要产品】科创、编程、赛事 | |
【核心特点】为青少年提供优质的科创学习规划。 | |
【核心亮点】高效提供更优质的赛事解决方案——快速赋能当地传统教育企业转型智能化教育赛道。""", | |
"""天天乐学创办于2015年6月,总部位于北京,创始团队均来自清北等知名高校,旗下产品天天乐学定制app,主打立体化教学和趣味性学习,可深度匹配教学需求,实现个性化教学。目前合作校近三万家,遍布全国31个省,300座城市,是国内知名的教育科技公司。 | |
【主要产品】独立教师·机构·绘本馆英语APP定制 | |
【核心特点】内容深度定制,帮助机构实现线上线下联动教学 | |
""", | |
"""【】星瀚之光是明德传承集团旗下文化传媒品牌 | |
成立于2023年,立足深圳,布局全国九大中心城市,是国内首家定位于价值观驱动的P产业链开发的MCN机构,拥有知识P孵化事业群和商业P孵化事业群两大主营板块,旗下涵盖了创始人P孵化直播电商、私域消费电商、企业整合营销、艺人经纪等业务,致力于成为中国规模最大的P生态搭建领军企业。 | |
【】坚守“自利利他、奋斗为本、价值为纲、成就客户”的企业价值观 | |
公司成立以来发展迅猛,迅速孵化出各大赛道的多个头部P,霸榜各大短视频平台榜单,包括:有家庭教育赛道:白瑞(家庭教育赛道头部主播);身心灵赛道:尚致胜(新中式心理学首创者);女性成长赛道:黄欢;商业赛道:苏建诚(亚细亚集团联合创始人)、王怀南(宝宝树创始人)、杨晓燕(长江商学院助理院长)、何慕(联众智达董事长)等;国学赛道:沈德斌(了凡四训学者)、钱锦国老师等。""", | |
"""学趋坊是一家为学校、机构、智能自习室等提供在线化和智能化解决方案的数字化教育内容供应商。已经为多个知名的在线教育品牌、智能自习室品牌提供0DM和OEM服务,产品用户累计超过1000万。 | |
【主要产品】智能校本 | |
【核心特点】为本地化内容插上科技的翅膀。 | |
【核心亮点】高质量高效本地化教研—快速赋能当地传统教育企业转型智能化教育赛道。""", | |
] | |
logo_path_list = [ | |
"src/assets/logo/学区房logo.png", | |
"src/assets/logo/星瀚之光logo.jpg", | |
"src/assets/logo/软科起点logo.png", | |
"src/assets/logo/天天乐学logo.png", | |
] | |
slogan_list = ["探索科技,启迪未来。", "价值观驱动的知识P全域孵化领先平台", "定制英语APP学习系统"] | |
FOREGROUND_ID = 3 # 0, 1, 2, 3 | |
SLOGAN_ID = 1 # 0, 1, 2 | |
TEXT_ID = 2 # 0, 1, 2, 3 | |
LOGO_LINK = "https://weboffice-temporary.ks3-cn-beijing.wpscdn.cn/thumbnail/tJUjbgWdD6R5k5rECKka1yK_100.png?Expires=1721322000&KSSAccessKeyId=AKLTmoJhggaFT1CHuozGZqbC&Signature=qZCdG1n42uwCFGJ8%2BAlF6VK8KEM%3D&response-cache-control=public%2Cmax-age%3D86400" | |
name, titles = "王千里", "全国政协文员\n好好先生" | |
PHOTO_LINK = "https://weboffice-temporary.ks3-cn-beijing.wpscdn.cn/thumbnail/tpcdnbPEKEJ3ge831dSHEd3_100.png?Expires=1720890000&KSSAccessKeyId=AKLTmoJhggaFT1CHuozGZqbC&Signature=jeKkEy%2BTx3BAMNzqrmjUbg6uBK8%3D&response-cache-control=public%2Cmax-age%3D86400" | |
ORIGINAL_PHOTO_LINK = "https://weboffice-temporary.ks3-cn-beijing.wpscdn.cn/thumbnail/tn6C9byoghgm8cnQYtShMdb_100.jpeg?Expires=1723395600&KSSAccessKeyId=AKLTmoJhggaFT1CHuozGZqbC&Signature=%2BJX%2BaPumafx8mXpV7wfN1Z3FVpA%3D&response-cache-control=public%2Cmax-age%3D86400" | |
conference_name = "教育行业规范管理发展论坛" | |
conference_name_eng = "2024 World Education Conference" | |
conference_name_full = "2024世界教育者大会上海主会" | |
conference_info = [ | |
["指导单位", "WEC教育者大会组委会、上海教科院民办教育研究所"], | |
["主办单位", "上海市民办教育协会、长三角教育发展研究院\上海市民办教育协会、长三角教育发展研究院\上海市民办教育协会、长三角教育发展研究院"], | |
["论坛时间", "2024年8月21日上午9:00-12:30"], | |
["论坛地址", "国家会展中心4.2号馆A"], | |
] | |
conference_rundown = [ | |
{ | |
"type": "主持人", | |
"title": "上海市教科规划办副主任", | |
"people": [ | |
{ | |
"name": "方法名", | |
"title": "上海市教科规划办副主任\n上海教科院改革发展研究部研究员\n华东师范大学国家宏观教育研究院博士生导师", | |
"photo_link": ORIGINAL_PHOTO_LINK, | |
}, | |
{ | |
"name": "方法名", | |
"title": "上海市教科规划办副主任\n上海教科院改革发展研究部研究员\n华东师范大学国家宏观教育研究院博士生导师", | |
"photo_link": "", | |
}, | |
], | |
"start_time": "9:00:00", | |
"end_time": "9:20:00", | |
}, | |
{ | |
"type": "致辞", | |
"title": "上海市教科规划办副主", | |
"people": [ | |
{ | |
"name": "方法名", | |
"title": "上海市教科规划办副主任", | |
"photo_link": ORIGINAL_PHOTO_LINK, | |
} | |
], | |
"start_time": "9:00:00", | |
"end_time": "9:20:00", | |
}, { | |
"type": "致辞", | |
"title": "上海市教科规划办副主任上海市教科规划办副主任上海市教科规", | |
"people": [ | |
{ | |
"name": "方法名", | |
"title": "上海市教科规划办副主任", | |
"photo_link": ORIGINAL_PHOTO_LINK, | |
} | |
], | |
"start_time": "9:00:00", | |
"end_time": "9:20:00", | |
}, | |
] | |
people = [ | |
{ | |
"name": "方法名", | |
"title": "上海市教科规划办副主任\n上海教科院改革发展研究部研究员\n华东师范大学国家宏观教育研究院博士生导师", | |
"photo_link": ORIGINAL_PHOTO_LINK, | |
}, | |
{ | |
"name": "方法名", | |
"title": "上海市教科规划办副主任\n上海教科院改革发展研究部研究员\n华东师范大学国家宏观教育研究院博士生导师", | |
"photo_link": ORIGINAL_PHOTO_LINK, | |
}, | |
{ | |
"name": "方法名", | |
"title": "上海市教科规划办副主任\n上海教科院改革发展研究部研究员\n华东师范大学国家宏观教育研究院博士生导师", | |
"photo_link": ORIGINAL_PHOTO_LINK, | |
}, | |
{ | |
"name": "方法名", | |
"title": "上海市教科规划办副主任\n上海教科院改革发展研究部研究员\n华东师范大学国家宏观教育研究院博士生导师", | |
"photo_link": ORIGINAL_PHOTO_LINK, | |
}, | |
{ | |
"name": "方法名", | |
"title": "上海市教科规划办副主任\n上海教科院改革发展研究部研究员\n华东师范大学国家宏观教育研究院博士生导师", | |
"photo_link": ORIGINAL_PHOTO_LINK, | |
}, | |
{ | |
"name": "方法名", | |
"title": "上海市教科规划办副主任", | |
"photo_link": ORIGINAL_PHOTO_LINK, | |
}, | |
{ | |
"name": "方法名", | |
"title": "上海市教科规划办副主任", | |
"photo_link": ORIGINAL_PHOTO_LINK, | |
}, | |
{ | |
"name": "方法名", | |
"title": "上海市教科规划办副主任", | |
"photo_link": ORIGINAL_PHOTO_LINK, | |
}, | |
{ | |
"name": "方法名", | |
"title": "上海市教科规划办副主任", | |
"photo_link": ORIGINAL_PHOTO_LINK | |
}, | |
{ | |
"name": "方法名", | |
"title": "上海市教科规划办副主任", | |
"photo_link": ORIGINAL_PHOTO_LINK, | |
}, | |
] | |
# res = requests.get(ORIGINAL_PHOTO_LINK) | |
# # guest_photo = Image.open(io.BytesIO(res.content)).convert("RGBA") | |
# original_guest_photo = Image.open(io.BytesIO(res.content)).convert("RGBA") | |
# guest_photo = rm_img_bg(original_guest_photo) | |
# compose_guest_poster(name, guest_photo, titles) | |
# for file in os.listdir("src/assets/GuestPoster/original_guest"): | |
# image = Image.open(os.path.join("src/assets/GuestPoster/original_guest", file)) | |
# image = rm_img_bg(image) | |
# image.save(os.path.join("src/assets/output", file.replace("jpg", "png"))) | |
# image = Image.open('src/assets/苏建诚.png') | |
# width, height = image.size | |
# circle_center = (width // 2, height // 4) | |
# circle_radius = min(width, height//2) // 2 | |
# round_img = crop_round_img(image, circle_center, circle_radius) | |
# print(round_img.size) | |
# res = requests.get(ORIGINAL_PHOTO_LINK) | |
# photo = Image.open(io.BytesIO(res.content)).convert("RGBA") | |
# make_exhibitor_poster(LOGO_LINK, slogan_list[SLOGAN_ID], text_list[TEXT_ID]) | |
# make_guest_poster(name, "", "", ORIGINAL_PHOTO_LINK, titles) | |
# make_conference_poster(conference_name, conference_name_eng, conference_name_full, conference_info, conference_rundown) | |
# 定义圆心和半径 | |
# res = requests.get(ORIGINAL_PHOTO_LINK) | |
# print(res) | |
# with open("src/assets/output/guest_compose_poster.png", "wb") as f: | |
# f.write(res.content) | |
# photo = Image.open(io.BytesIO(res.content)).convert("RGBA") | |
# getGuestComposePoster("全球教育者公论教育", "EDUCATORS AROUND THE GLOBE TALKING ABOUT EDUCATION", people) | |
getGuestEScreen("full", conference_rundown[0]["people"][0]) | |