fix: hybrid SVG+PIL PNG image rendering with CJK fonts, prompt URL reference
This commit is contained in:
+64
-15
@@ -3,9 +3,67 @@
|
||||
多平台文章配图生成器
|
||||
为知乎/公众号/小红书生成平台风格的 SVG 配图(base64 内联,无需外部资源)
|
||||
"""
|
||||
import base64, math, textwrap, re
|
||||
import base64, math, textwrap, re, io, os
|
||||
from typing import List, Optional
|
||||
|
||||
CJK_FONT = "/usr/share/fonts/google-noto-cjk/NotoSansCJKsc-Regular.otf"
|
||||
CJK_FONT_BOLD = "/usr/share/fonts/google-noto-cjk/NotoSansCJKsc-Bold.otf"
|
||||
|
||||
|
||||
def _svg_to_png_with_pil(svg: str) -> bytes:
|
||||
"""用 cairosvg 渲染背景 + Pillow 叠加中文文字"""
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import cairosvg
|
||||
|
||||
text_elements = []
|
||||
def _extract_text(m):
|
||||
attrs = dict(re.findall(r'([\w-]+)="([^"]*)"', m.group(1)))
|
||||
text_elements.append({
|
||||
'x': int(float(attrs.get('x', 0))),
|
||||
'y': int(float(attrs.get('y', 0))),
|
||||
'size': int(float(attrs.get('font-size', 14))),
|
||||
'weight': attrs.get('font-weight', 'normal'),
|
||||
'fill': attrs.get('fill', '#000'),
|
||||
'opacity': float(attrs.get('opacity', 1) or 1),
|
||||
'text': m.group(2),
|
||||
})
|
||||
return ''
|
||||
|
||||
svg_no_text = re.sub(r'<text\b([^>]*)>([^<]*)</text>', _extract_text, svg)
|
||||
|
||||
png = cairosvg.svg2png(bytestring=svg_no_text.encode('utf-8'))
|
||||
img = Image.open(io.BytesIO(png)).convert('RGBA')
|
||||
draw = ImageDraw.Draw(img, 'RGBA')
|
||||
|
||||
for el in text_elements:
|
||||
if not el['text'].strip():
|
||||
continue
|
||||
font_path = CJK_FONT_BOLD if el['weight'] in ('bold', '700', '800', '900') else CJK_FONT
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, el['size'])
|
||||
except Exception:
|
||||
font = ImageFont.load_default()
|
||||
fill_color = el['fill'].lstrip('#')
|
||||
try:
|
||||
r, g, b = tuple(int(fill_color[i:i+2], 16) for i in (0, 2, 4))
|
||||
except Exception:
|
||||
r, g, b = 0, 0, 0
|
||||
a = int(255 * el['opacity'])
|
||||
_, _, tw, th = draw.textbbox((0, 0), el['text'], font=font)
|
||||
draw.text((el['x'], el['y'] - int(el['size'] * 0.85)), el['text'], font=font, fill=(r, g, b, a))
|
||||
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, 'PNG')
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def _to_base64(svg: str) -> str:
|
||||
try:
|
||||
png = _svg_to_png_with_pil(svg)
|
||||
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')
|
||||
|
||||
PLATFORM_STYLES = {
|
||||
"zhihu": {
|
||||
"primary": "#0084ff",
|
||||
@@ -39,7 +97,7 @@ PLATFORM_STYLES = {
|
||||
},
|
||||
}
|
||||
|
||||
FONT = "-apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', sans-serif"
|
||||
FONT = "-apple-system, BlinkMacSystemFont, 'Noto Sans CJK SC', 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', sans-serif"
|
||||
|
||||
|
||||
def _wrap_chinese(text: str, chars_per_line: int = 14) -> List[str]:
|
||||
@@ -61,15 +119,6 @@ def _wrap_chinese(text: str, chars_per_line: int = 14) -> List[str]:
|
||||
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("'", ''')
|
||||
|
||||
@@ -166,11 +215,11 @@ def generate_data_highlight(platform: str, number: str, label: str) -> str:
|
||||
|
||||
|
||||
def insert_lead_image(html: str, platform: str, title: str, field: str) -> str:
|
||||
"""在 HTML 正文开头插入头图(仅必要环节)"""
|
||||
"""在 HTML 正文开头插入头图(位于 h1 之前)"""
|
||||
lead = generate_lead(platform, title, field)
|
||||
h1_end = html.find('</h1>')
|
||||
if h1_end != -1:
|
||||
html = html[:h1_end + 5] + '\n' + lead + html[h1_end + 5:]
|
||||
h1_start = html.find('<h1>')
|
||||
if h1_start != -1:
|
||||
html = html[:h1_start] + lead + '\n' + html[h1_start:]
|
||||
else:
|
||||
html = lead + html
|
||||
return html
|
||||
|
||||
Reference in New Issue
Block a user