#!/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'

\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''''''
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''''''
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''''''
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''''''
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"},
}