Initial commit
This commit is contained in:
326
backup/txt_to_image.py
Normal file
326
backup/txt_to_image.py
Normal file
@@ -0,0 +1,326 @@
|
||||
#########################################################
|
||||
## @file : txt_to_image.py
|
||||
## @desc : text content convert to image
|
||||
## @create : 2025/6/22
|
||||
## @author : Chengan,doubao AI
|
||||
## @email : douboer@gmail.com
|
||||
#########################################################
|
||||
|
||||
from logger_utils import CommonLogger
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
||||
import textwrap
|
||||
import random
|
||||
import os
|
||||
from config import fonts_path, templates, styles
|
||||
|
||||
class TextToImage:
|
||||
"""将文字内容转换为美观图片的类"""
|
||||
def __init__(self, log_file=None):
|
||||
"""初始化字体和模板路径"""
|
||||
self.logger = CommonLogger(log_file).get_logger()
|
||||
self.fonts_path = fonts_path
|
||||
self.templates = templates
|
||||
self.target_size = (1244, 1660) # 目标尺寸:1244x1660
|
||||
|
||||
def create_image(self, title, content, subtitle=None, signature=None,
|
||||
template='minimal',
|
||||
output_path='output.png'):
|
||||
"""创建文字图片"""
|
||||
try:
|
||||
# 确保模板存在
|
||||
if template not in self.templates:
|
||||
raise ValueError(f"模板 '{template}' 不存在")
|
||||
template_data = self.templates[template]
|
||||
|
||||
# 处理背景图片尺寸
|
||||
background = self._process_background(template_data['background'])
|
||||
draw = ImageDraw.Draw(background)
|
||||
width, height = background.size
|
||||
padding = template_data['padding']
|
||||
|
||||
# 从config获取styles
|
||||
title_style = styles['title']
|
||||
subtitle_style = styles['subtitle']
|
||||
body_style = styles['body']
|
||||
signature_style = styles['signature']
|
||||
top_offset = styles['top_offset']
|
||||
bottom_offset = styles['bottom_offset']
|
||||
paragraph_spacing = styles['paragraph_spacing'] # 获取段间距
|
||||
|
||||
# 检查字体文件是否存在
|
||||
for key, path in self.fonts_path.items():
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError(f"字体文件 '{path}' 不存在,请确认已放入fonts目录")
|
||||
|
||||
# 加载字体
|
||||
title_font = ImageFont.truetype(self.fonts_path['title'], title_style['size'])
|
||||
subtitle_font = ImageFont.truetype(self.fonts_path['subtitle'], subtitle_style['size'])
|
||||
body_font = ImageFont.truetype(self.fonts_path['body'], body_style['size'])
|
||||
signature_font = ImageFont.truetype(self.fonts_path['signature'], signature_style['size'])
|
||||
|
||||
# 绘制标题
|
||||
title_x = (width - self._get_text_width(title, title_font, title_style['letter_spacing'])) // 2
|
||||
title_y = padding + title_style['top_spacing'] # 使用 top_spacing
|
||||
self._draw_text_with_spacing(draw, (title_x, title_y), title,
|
||||
title_font, title_style['color'], title_style['letter_spacing'])
|
||||
|
||||
# 计算标题高度
|
||||
title_bbox = draw.textbbox((0, 0), title, font=title_font)
|
||||
title_height = title_bbox[3] - title_bbox[1]
|
||||
|
||||
# 绘制副标题
|
||||
current_y = title_y + title_height + title_style['bottom_spacing']
|
||||
if subtitle:
|
||||
subtitle_x = (width - self._get_text_width(subtitle, subtitle_font, subtitle_style['letter_spacing'])) // 2
|
||||
self._draw_text_with_spacing(draw, (subtitle_x, current_y), subtitle,
|
||||
subtitle_font, subtitle_style['color'], subtitle_style['letter_spacing'])
|
||||
subtitle_bbox = draw.textbbox((0, 0), subtitle, font=subtitle_font)
|
||||
subtitle_height = subtitle_bbox[3] - subtitle_bbox[1]
|
||||
current_y += subtitle_height + subtitle_style['bottom_spacing']
|
||||
|
||||
# 分割内容为段落
|
||||
paragraphs = content.split('\n\n')
|
||||
page_num = 1
|
||||
output_paths = []
|
||||
for i, paragraph in enumerate(paragraphs):
|
||||
if i > 0: # 如果不是第一个段落,添加段间距
|
||||
current_y += paragraph_spacing
|
||||
|
||||
if current_y + self._get_paragraph_height(paragraph, body_font, body_style) > height - bottom_offset:
|
||||
# 绘制签名
|
||||
if signature:
|
||||
self._draw_signature(draw, width, height, padding, signature, signature_font, signature_style)
|
||||
# 保存当前图片
|
||||
output_path_current = output_path.replace('.png', f'_{page_num}.png')
|
||||
background.save(output_path_current)
|
||||
self.logger.info(f"图片已保存至: {output_path_current}")
|
||||
output_paths.append(output_path_current)
|
||||
# 创建新的图片
|
||||
background = self._process_background(template_data['background'])
|
||||
draw = ImageDraw.Draw(background)
|
||||
current_y = top_offset
|
||||
page_num += 1
|
||||
|
||||
# 绘制段落
|
||||
max_width = width - padding * 2
|
||||
wrapped_text = self._wrap_text(paragraph, body_font, max_width, body_style['letter_spacing'])
|
||||
self._draw_multiline_text_with_spacing(
|
||||
draw, (padding, current_y), wrapped_text,
|
||||
body_font, body_style['color'],
|
||||
body_style['letter_spacing'], body_style['line_spacing']
|
||||
)
|
||||
|
||||
# 计算段落高度
|
||||
text_bbox = draw.textbbox((0, 0), wrapped_text, font=body_font)
|
||||
text_height = (text_bbox[3] - text_bbox[1]) + (body_style['line_spacing'] * (wrapped_text.count('\n') or 1))
|
||||
current_y += text_height + body_style['line_spacing']
|
||||
|
||||
# 绘制签名
|
||||
if signature:
|
||||
self._draw_signature(draw, width, height, padding, signature, signature_font, signature_style)
|
||||
|
||||
# 保存最后一张图片
|
||||
output_path_current = output_path.replace('.png', f'_{page_num}.png')
|
||||
background.save(output_path_current)
|
||||
self.logger.info(f"图片已保存至: {output_path_current}")
|
||||
output_paths.append(output_path_current)
|
||||
|
||||
return output_paths
|
||||
except Exception as e:
|
||||
self.logger.exception(f"创建图片时发生错误: {e}")
|
||||
return None
|
||||
|
||||
def _get_text_width(self, text, font, letter_spacing):
|
||||
"""计算包含字间距的文本总宽度"""
|
||||
if not text:
|
||||
return 0
|
||||
bbox = font.getbbox(text)
|
||||
return bbox[2] - bbox[0] + (letter_spacing * (len(text) - 1))
|
||||
|
||||
def _draw_text_with_spacing(self, draw, position, text, font, fill, letter_spacing):
|
||||
"""绘制包含字间距的文本"""
|
||||
x, y = position
|
||||
for char in text:
|
||||
draw.text((x, y), char, font=font, fill=fill)
|
||||
# 获取字符宽度并加上字间距
|
||||
char_width = font.getlength(char)
|
||||
x += char_width + letter_spacing
|
||||
|
||||
def _draw_multiline_text_with_spacing(self, draw, position, text, font, fill,
|
||||
letter_spacing, line_spacing):
|
||||
"""绘制包含字间距和行间距的多行文本"""
|
||||
x, y = position
|
||||
lines = text.split('\n')
|
||||
for line in lines:
|
||||
self._draw_text_with_spacing(draw, (x, y), line, font, fill, letter_spacing)
|
||||
# 获取行高并加上行间距
|
||||
line_bbox = draw.textbbox((0, 0), line, font=font)
|
||||
line_height = line_bbox[3] - line_bbox[1]
|
||||
y += line_height + line_spacing
|
||||
|
||||
def _add_decorations(self, draw, width, height, template_data):
|
||||
"""添加装饰元素"""
|
||||
padding = template_data['padding']
|
||||
color = (30, 30, 30) # 默认装饰颜色
|
||||
# 顶部和底部添加细线条
|
||||
#draw.line([(padding, padding//2), (width-padding, padding//2)], fill=color, width=1)
|
||||
#draw.line([(padding, height-padding//2), (width-padding, height-padding//2)], fill=color, width=1)
|
||||
|
||||
# 添加几何装饰点
|
||||
for _ in range(8):
|
||||
x = random.randint(padding, width-padding)
|
||||
y = random.randint(padding, height-padding)
|
||||
draw.ellipse([(x-2, y-2), (x+2, y+2)], fill=color)
|
||||
|
||||
def _wrap_text(self, text, font, max_width, letter_spacing):
|
||||
"""根据最大宽度和字间距智能换行"""
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
avg_char_width = font.getlength('a') + letter_spacing
|
||||
max_chars_per_line = int(max_width // avg_char_width)
|
||||
|
||||
wrapped_text = ""
|
||||
lines = text.split('\n')
|
||||
|
||||
for line in lines:
|
||||
if not line:
|
||||
wrapped_text += "\n\n"
|
||||
continue
|
||||
|
||||
words = list(line)
|
||||
current_line = ""
|
||||
current_width = 0
|
||||
|
||||
for word in words:
|
||||
word_width = font.getlength(word) + letter_spacing
|
||||
if current_width + word_width > max_width:
|
||||
if current_line:
|
||||
wrapped_text += current_line + "\n"
|
||||
current_line = word
|
||||
current_width = word_width
|
||||
else:
|
||||
wrapped_text += word + "\n"
|
||||
current_line = ""
|
||||
current_width = 0
|
||||
else:
|
||||
current_line += word
|
||||
current_width += word_width
|
||||
if current_line:
|
||||
wrapped_text += current_line + "\n"
|
||||
|
||||
return wrapped_text.strip()
|
||||
|
||||
def _process_background(self, background_path):
|
||||
"""处理背景图片,将宽度调整为1440并等比例缩放高度,然后裁剪或扩展为目标尺寸1244x1660"""
|
||||
target_width, target_height = self.target_size
|
||||
try:
|
||||
# 打开背景图片
|
||||
background = Image.open(background_path).convert("RGBA")
|
||||
bg_width, bg_height = background.size
|
||||
|
||||
# 调整宽度为1440并等比例缩放高度
|
||||
if bg_width != 1440:
|
||||
ratio = 1440 / bg_width
|
||||
new_height = int(bg_height * ratio)
|
||||
background = background.resize((1440, new_height), Image.LANCZOS)
|
||||
|
||||
bg_width, bg_height = background.size
|
||||
|
||||
# 如果图片尺寸大于目标尺寸,进行裁剪
|
||||
if bg_width >= target_width and bg_height >= target_height:
|
||||
# 计算裁剪区域(居中裁剪)
|
||||
left = (bg_width - target_width) // 2
|
||||
top = (bg_height - target_height) // 2
|
||||
right = left + target_width
|
||||
bottom = top + target_height
|
||||
background = background.crop((left, top, right, bottom))
|
||||
|
||||
# 如果图片尺寸小于目标尺寸,进行扩展(白色填充)
|
||||
else:
|
||||
new_background = Image.new("RGBA", self.target_size, (255, 255, 255, 255))
|
||||
# 计算粘贴位置(居中)
|
||||
paste_x = (target_width - bg_width) // 2
|
||||
paste_y = (target_height - bg_height) // 2
|
||||
new_background.paste(background, (paste_x, paste_y))
|
||||
background = new_background
|
||||
|
||||
except Exception as e:
|
||||
# 如果背景图片处理失败,创建默认尺寸的白色背景
|
||||
self.logger.exception(f"背景图片处理失败: {e}")
|
||||
background = Image.new("RGBA", self.target_size, (255, 255, 255, 255))
|
||||
|
||||
return background
|
||||
|
||||
def _get_paragraph_height(self, paragraph, font, style):
|
||||
"""计算段落的高度"""
|
||||
max_width = self.target_size[0] - 2 * self.templates['minimal']['padding']
|
||||
wrapped_text = self._wrap_text(paragraph, font, max_width, style['letter_spacing'])
|
||||
text_bbox = ImageDraw.Draw(Image.new("RGBA", self.target_size)).textbbox((0, 0), wrapped_text, font=font)
|
||||
text_height = (text_bbox[3] - text_bbox[1]) + (style['line_spacing'] * (wrapped_text.count('\n') or 1))
|
||||
return text_height
|
||||
|
||||
def _draw_signature(self, draw, width, height, padding, signature, signature_font, signature_style):
|
||||
"""绘制签名"""
|
||||
signature_width = self._get_text_width(signature, signature_font, signature_style['letter_spacing'])
|
||||
signature_bbox = draw.textbbox((0, 0), signature, font=signature_font)
|
||||
signature_height = signature_bbox[3] - signature_bbox[1]
|
||||
|
||||
# 根据配置确定签名垂直位置
|
||||
if signature_style['vertical_position'] == 'bottom':
|
||||
# 固定在底部
|
||||
signature_y = height - padding - signature_height - signature_style['offset']
|
||||
else:
|
||||
# 跟随内容流动(原逻辑)
|
||||
signature_y = current_y + 40
|
||||
|
||||
# 根据配置确定签名水平位置
|
||||
if signature_style['position'] == 'left':
|
||||
signature_x = padding
|
||||
elif signature_style['position'] == 'center':
|
||||
signature_x = (width - signature_width) // 2
|
||||
else: # 'right'
|
||||
signature_x = width - padding - signature_width
|
||||
|
||||
self._draw_text_with_spacing(draw, (signature_x, signature_y), signature,
|
||||
signature_font, signature_style['color'], signature_style['letter_spacing'])
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 示例内容
|
||||
title = "夏日阅读清单"
|
||||
subtitle = "2025年必读书单推荐"
|
||||
content = """日本寺院有抽签的风尚,大概也是收入不菲的创收项目吧。一个个签位,标了价格,无人售票,这种方式挺好,没有买卖的功利和压力。
|
||||
|
||||
不免俗,抽了支,下下,中间有句,大意是人财分离。虽不作兴这个,还是略有不悦。
|
||||
|
||||
逛完突然想着去吃抹茶小点,老杨发现转角处就有一家抹茶专业店,可惜刚打烊。边上小店点了一个撒着浅草字样的冰激凌,无惊喜。
|
||||
|
||||
途经一家电玩店,别有洞天,一排排整齐划一,机器挨着机器,每一排机器造型不同,应该超过500台,空间被利用到极致。充斥着机器电音,令人想起工业朋
|
||||
克。一簇簇坐着的玩家,电子烟是标配,60%是中老年人,秃头大爷和白发老奶不少见,并无少年,颠覆我对游戏厅的印象。
|
||||
|
||||
这里机器是掌控者,人是机器的有机体延伸,机器设定规则,发出吼叫,刺激神经,填满有机体空虚孤独的心。比起从站台跳下去,被机器奴役也并不算太差,这样想来,风月场也好,游戏厅也好,经营者都有功德。"""
|
||||
signature = "@刀波儿"
|
||||
|
||||
# 确保目录存在
|
||||
os.makedirs('fonts', exist_ok=True)
|
||||
os.makedirs('backgrounds', exist_ok=True)
|
||||
|
||||
# 创建简单背景(如果模板不存在)
|
||||
if not os.path.exists('backgrounds/minimal.jpg'):
|
||||
img = Image.new('RGB', (1244, 1660), color=(240, 240, 240))
|
||||
draw = ImageDraw.Draw(img)
|
||||
for i in range(0, 1660, 20):
|
||||
draw.line([(0, i), (1244, i)], fill=(230, 230, 230))
|
||||
img.save('backgrounds/minimal.jpg')
|
||||
|
||||
# 生成图片
|
||||
converter = TextToImage(log_file='logs/txt_to_image.log')
|
||||
output_paths = converter.create_image(
|
||||
title=title,
|
||||
subtitle=subtitle,
|
||||
content=content,
|
||||
signature=signature,
|
||||
template='minimal',
|
||||
output_path='temp/reading_list.png'
|
||||
)
|
||||
print("生成的图片路径:", output_paths)
|
||||
Reference in New Issue
Block a user