#!/usr/bin/env python3 """ 多平台文章配图生成器 为知乎/公众号/小红书生成平台风格的 SVG 配图(base64 内联,无需外部资源) """ import base64, math, textwrap, re from typing import List, Optional PLATFORM_STYLES = { "zhihu": { "primary": "#0084ff", "primary_rgb": "0, 132, 255", "accent": "#e8f4fd", "gradient_start": "#e8f4fd", "gradient_end": "#f5f9ff", "card_bg": "#ffffff", "title_color": "#1a1a1a", "dim_color": "#c0c4cc", }, "wechat": { "primary": "#07c160", "primary_rgb": "7, 193, 96", "accent": "#f0faf4", "gradient_start": "#f0faf4", "gradient_end": "#e8f5ee", "card_bg": "#ffffff", "title_color": "#1a1a1a", "dim_color": "#c0c4cc", }, "xiaohongshu": { "primary": "#ff2442", "primary_rgb": "255, 36, 66", "accent": "#fff5f5", "gradient_start": "#fff5f5", "gradient_end": "#fff0f0", "card_bg": "#ffffff", "title_color": "#262626", "dim_color": "#bfbfbf", }, } FONT = "-apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', sans-serif" def _wrap_chinese(text: str, chars_per_line: int = 14) -> List[str]: """将中文文本按字数折行,尽量在标点处断开""" if not text: return [""] lines = [] remainder = text while len(remainder) > chars_per_line: chunk = remainder[:chars_per_line] # 尝试在最后一个标点处断开 cut = max(chunk.rfind(c) + 1 for c in (",", "、", "。", "!", "?", ":", ";", ")", " ", "—") if c in chunk[:-1]) if cut <= 0: cut = chars_per_line lines.append(remainder[:cut].strip()) remainder = remainder[cut:].strip() if remainder: lines.append(remainder) return lines def _to_base64(svg: str) -> str: try: import cairosvg png = cairosvg.svg2png(bytestring=svg.encode('utf-8')) return 'data:image/png;base64,' + base64.b64encode(png).decode('ascii') except Exception: return 'data:image/svg+xml;base64,' + base64.b64encode(svg.encode('utf-8')).decode('ascii') def _alt_attr(text: str) -> str: return text.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''') def _img_tag(src: str, alt: str, width: int = 1080) -> str: return f'

{_alt_attr(alt)}

\n' def generate_lead(platform: str, title: str, field: str = "", brand: str = "宇之然") -> str: """生成文章头图(1080×600)""" s = PLATFORM_STYLES.get(platform, PLATFORM_STYLES["zhihu"]) lines = _wrap_chinese(title, 16) title_lines = "" y_start = 160 for i, line in enumerate(lines[:3]): title_lines += f'{_alt_attr(line)}\n' field_badge = "" if field: field_badge = f''' {_alt_attr(field)}''' svg = f''' {title_lines} {field_badge} {_alt_attr(brand)} ''' return _img_tag(_to_base64(svg), title) def generate_section_card(platform: str, section_title: str, section_num: int) -> str: """生成章节分隔图(800×160)""" s = PLATFORM_STYLES.get(platform, PLATFORM_STYLES["zhihu"]) num_text = f"{section_num:02d}" svg = f''' {num_text} {_alt_attr(section_title)} ''' return _img_tag(_to_base64(svg), section_title, 800) def generate_quote_card(platform: str, quote: str, source: str = "") -> str: """生成金句卡片(800×280)""" s = PLATFORM_STYLES.get(platform, PLATFORM_STYLES["zhihu"]) lines = _wrap_chinese(quote, 20) quote_lines = "" y_start = 110 for i, line in enumerate(lines[:4]): quote_lines += f'{_alt_attr(line)}\n' source_line = "" if source: source_line = f'{_alt_attr(f"— {source}")}' svg = f''' " {quote_lines} {source_line} ''' return _img_tag(_to_base64(svg), f"金句:{quote[:30]}", 800) def generate_data_highlight(platform: str, number: str, label: str) -> str: """生成数据高亮卡片(800×220)""" s = PLATFORM_STYLES.get(platform, PLATFORM_STYLES["zhihu"]) svg = f''' {_alt_attr(number)} {_alt_attr(label)} ''' return _img_tag(_to_base64(svg), label, 800) def insert_lead_image(html: str, platform: str, title: str, field: str) -> str: """在 HTML 正文开头插入头图(仅必要环节)""" lead = generate_lead(platform, title, field) h1_end = html.find('') if h1_end != -1: html = html[:h1_end + 5] + '\n' + lead + html[h1_end + 5:] else: html = lead + html return html # 平台特定的配图密度 PLATFORM_IMAGE_COUNTS = { "zhihu": {"lead": True, "sections": True, "quotes": 0, "data": 0, "density": "medium"}, "wechat": {"lead": True, "sections": True, "quotes": 0, "data": 0, "density": "medium"}, "xiaohongshu": {"lead": True, "sections": True, "quotes": 0, "data": 0, "density": "high"}, }