Files
xhsautopublisher/txt_to_image.py
2025-09-05 17:20:14 +08:00

327 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

#########################################################
## @file : txt_to_image.py
## @desc : text content convert to image
## @create : 2025/6/22
## @author : Chengandoubao 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)