######################################################### ## @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)