Files
bettafish-company/ReportEngine/renderers/math_to_svg.py
T

217 lines
6.0 KiB
Python

"""
LaTeX 数学公式转 SVG 渲染器
使用 matplotlib 将 LaTeX 公式渲染为 SVG 格式,用于 PDF 导出
"""
import io
import logging
from typing import Optional
import matplotlib
import matplotlib.pyplot as plt
from matplotlib import mathtext
# 使用非交互式后端
matplotlib.use('Agg')
logger = logging.getLogger(__name__)
class MathToSVG:
"""将 LaTeX 数学公式转换为 SVG 的转换器"""
def __init__(self, font_size: int = 14, color: str = 'black'):
"""
初始化公式转换器
Args:
font_size: 字体大小(点)
color: 文字颜色
"""
self.font_size = font_size
self.color = color
def convert_to_svg(self, latex: str, display_mode: bool = True) -> Optional[str]:
"""
将 LaTeX 公式转换为 SVG 字符串
Args:
latex: LaTeX 公式字符串(不包含 $$ 或 $ 符号)
display_mode: True 为显示模式(块级公式),False 为行内模式
Returns:
SVG 字符串,如果转换失败则返回 None
"""
try:
# 清理 LaTeX 字符串
latex = latex.strip()
if not latex:
logger.warning("空的 LaTeX 公式")
return None
# 创建图形
fig = plt.figure(figsize=(10, 2) if display_mode else (6, 1))
fig.patch.set_alpha(0) # 透明背景
# 渲染 LaTeX
# 使用 mathtext 进行渲染
if display_mode:
# 显示模式:居中,较大字体
text = fig.text(
0.5, 0.5,
f'${latex}$',
fontsize=self.font_size * 1.2,
color=self.color,
ha='center',
va='center',
usetex=False # 使用 matplotlib 内置的 mathtext 而非完整 LaTeX
)
else:
# 行内模式:左对齐,正常字体
text = fig.text(
0.1, 0.5,
f'${latex}$',
fontsize=self.font_size,
color=self.color,
ha='left',
va='center',
usetex=False
)
# 获取文本边界框
fig.canvas.draw()
bbox = text.get_window_extent(renderer=fig.canvas.get_renderer())
# 转换为英寸(matplotlib 使用的单位)
bbox_inches = bbox.transformed(fig.dpi_scale_trans.inverted())
# 调整图形大小以适应文本,添加边距
margin = 0.1 # 英寸
fig.set_size_inches(
bbox_inches.width + 2 * margin,
bbox_inches.height + 2 * margin
)
# 重新定位文本到中心
text.set_position((0.5, 0.5))
# 保存为 SVG
svg_buffer = io.StringIO()
plt.savefig(
svg_buffer,
format='svg',
bbox_inches='tight',
pad_inches=0.1,
transparent=True,
dpi=300
)
plt.close(fig)
# 获取 SVG 内容
svg_content = svg_buffer.getvalue()
svg_buffer.close()
return svg_content
except Exception as e:
logger.error(f"LaTeX 公式转换失败: {latex[:100]}... 错误: {str(e)}")
return None
def convert_inline_to_svg(self, latex: str) -> Optional[str]:
"""
将行内 LaTeX 公式转换为 SVG
Args:
latex: LaTeX 公式字符串
Returns:
SVG 字符串,如果转换失败则返回 None
"""
return self.convert_to_svg(latex, display_mode=False)
def convert_display_to_svg(self, latex: str) -> Optional[str]:
"""
将显示模式 LaTeX 公式转换为 SVG
Args:
latex: LaTeX 公式字符串
Returns:
SVG 字符串,如果转换失败则返回 None
"""
return self.convert_to_svg(latex, display_mode=True)
def convert_math_block_to_svg(
latex: str,
font_size: int = 16,
color: str = 'black'
) -> Optional[str]:
"""
便捷函数:将数学公式块转换为 SVG
Args:
latex: LaTeX 公式字符串
font_size: 字体大小
color: 文字颜色
Returns:
SVG 字符串,如果转换失败则返回 None
"""
converter = MathToSVG(font_size=font_size, color=color)
return converter.convert_display_to_svg(latex)
def convert_math_inline_to_svg(
latex: str,
font_size: int = 14,
color: str = 'black'
) -> Optional[str]:
"""
便捷函数:将行内数学公式转换为 SVG
Args:
latex: LaTeX 公式字符串
font_size: 字体大小
color: 文字颜色
Returns:
SVG 字符串,如果转换失败则返回 None
"""
converter = MathToSVG(font_size=font_size, color=color)
return converter.convert_inline_to_svg(latex)
if __name__ == "__main__":
# 测试代码
import sys
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# 测试公式
test_formulas = [
r"E = mc^2",
r"\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}",
r"\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}",
r"\sum_{i=1}^{n} i = \frac{n(n+1)}{2}",
]
converter = MathToSVG(font_size=16)
for i, formula in enumerate(test_formulas):
logger.info(f"测试公式 {i+1}: {formula}")
svg = converter.convert_display_to_svg(formula)
if svg:
# 保存到文件
filename = f"test_math_{i+1}.svg"
with open(filename, 'w', encoding='utf-8') as f:
f.write(svg)
logger.info(f"成功保存到 {filename}")
else:
logger.error(f"公式 {i+1} 转换失败")
logger.info("测试完成")