Optimize the Rendering of Inline Formulas, Subscripts and Superscripts, Bubble Charts, and Horizontal Bars
This commit is contained in:
@@ -160,6 +160,19 @@ class ChartToSVGConverter:
|
|||||||
if props.get('type'):
|
if props.get('type'):
|
||||||
chart_type = props['type']
|
chart_type = props['type']
|
||||||
|
|
||||||
|
# Chart.js v4已移除horizontalBar类型,这里自动降级为bar并设置横向坐标
|
||||||
|
horizontal_bar = False
|
||||||
|
if chart_type and str(chart_type).lower() == 'horizontalbar':
|
||||||
|
chart_type = 'bar'
|
||||||
|
horizontal_bar = True
|
||||||
|
|
||||||
|
# 支持通过indexAxis: 'y' 强制横向柱状图
|
||||||
|
if isinstance(props, dict):
|
||||||
|
options = props.get('options') or {}
|
||||||
|
index_axis = (options.get('indexAxis') or props.get('indexAxis') or '').lower()
|
||||||
|
if index_axis == 'y':
|
||||||
|
horizontal_bar = True
|
||||||
|
|
||||||
# 提取数据
|
# 提取数据
|
||||||
data = widget_data.get('data', {})
|
data = widget_data.get('data', {})
|
||||||
if not data:
|
if not data:
|
||||||
@@ -172,10 +185,16 @@ class ChartToSVGConverter:
|
|||||||
logger.debug("检测到词云图表,跳过chart_to_svg转换")
|
logger.debug("检测到词云图表,跳过chart_to_svg转换")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
render_method = getattr(self, f'_render_{chart_type}', None)
|
# 分派渲染方法,特殊处理横向柱状图
|
||||||
if not render_method:
|
if chart_type == 'bar':
|
||||||
logger.warning(f"不支持的图表类型: {chart_type}")
|
return self._render_bar(data, props, width, height, dpi, horizontal=horizontal_bar)
|
||||||
return None
|
elif chart_type == 'bubble':
|
||||||
|
return self._render_bubble(data, props, width, height, dpi)
|
||||||
|
else:
|
||||||
|
render_method = getattr(self, f'_render_{chart_type}', None)
|
||||||
|
if not render_method:
|
||||||
|
logger.warning(f"不支持的图表类型: {chart_type}")
|
||||||
|
return None
|
||||||
|
|
||||||
# 创建图表并转换为SVG
|
# 创建图表并转换为SVG
|
||||||
return render_method(data, props, width, height, dpi)
|
return render_method(data, props, width, height, dpi)
|
||||||
@@ -687,9 +706,10 @@ class ChartToSVGConverter:
|
|||||||
props: Dict[str, Any],
|
props: Dict[str, Any],
|
||||||
width: int,
|
width: int,
|
||||||
height: int,
|
height: int,
|
||||||
dpi: int
|
dpi: int,
|
||||||
|
horizontal: bool = False
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""渲染柱状图"""
|
"""渲染柱状图(支持横向barh)"""
|
||||||
try:
|
try:
|
||||||
labels = data.get('labels', [])
|
labels = data.get('labels', [])
|
||||||
datasets = data.get('datasets', [])
|
datasets = data.get('datasets', [])
|
||||||
@@ -703,44 +723,147 @@ class ChartToSVGConverter:
|
|||||||
colors = self._get_colors(datasets)
|
colors = self._get_colors(datasets)
|
||||||
|
|
||||||
# 计算柱子位置
|
# 计算柱子位置
|
||||||
x = np.arange(len(labels))
|
positions = np.arange(len(labels))
|
||||||
width_bar = 0.8 / len(datasets) if len(datasets) > 1 else 0.6
|
width_bar = 0.8 / len(datasets) if len(datasets) > 1 else 0.6
|
||||||
|
|
||||||
# 绘制每个数据系列
|
# 横向/纵向绘制
|
||||||
for i, dataset in enumerate(datasets):
|
for i, dataset in enumerate(datasets):
|
||||||
dataset_data = dataset.get('data', [])
|
dataset_data = dataset.get('data', [])
|
||||||
label = dataset.get('label', f'系列{i+1}')
|
label = dataset.get('label', f'系列{i+1}')
|
||||||
color = colors[i]
|
color = colors[i]
|
||||||
|
|
||||||
offset = (i - len(datasets)/2 + 0.5) * width_bar
|
offset = (i - len(datasets)/2 + 0.5) * width_bar
|
||||||
ax.bar(
|
|
||||||
x + offset,
|
|
||||||
dataset_data,
|
|
||||||
width_bar,
|
|
||||||
label=label,
|
|
||||||
color=color,
|
|
||||||
alpha=0.8,
|
|
||||||
edgecolor='white',
|
|
||||||
linewidth=0.5
|
|
||||||
)
|
|
||||||
|
|
||||||
# 设置x轴标签
|
if horizontal:
|
||||||
ax.set_xticks(x)
|
ax.barh(
|
||||||
ax.set_xticklabels(labels, rotation=45, ha='right')
|
positions + offset,
|
||||||
|
dataset_data,
|
||||||
|
height=width_bar,
|
||||||
|
label=label,
|
||||||
|
color=color,
|
||||||
|
alpha=0.8,
|
||||||
|
edgecolor='white',
|
||||||
|
linewidth=0.5
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ax.bar(
|
||||||
|
positions + offset,
|
||||||
|
dataset_data,
|
||||||
|
width_bar,
|
||||||
|
label=label,
|
||||||
|
color=color,
|
||||||
|
alpha=0.8,
|
||||||
|
edgecolor='white',
|
||||||
|
linewidth=0.5
|
||||||
|
)
|
||||||
|
|
||||||
|
# 轴标签/网格
|
||||||
|
if horizontal:
|
||||||
|
ax.set_yticks(positions)
|
||||||
|
ax.set_yticklabels(labels)
|
||||||
|
ax.invert_yaxis() # 与Chart.js横向排列保持一致
|
||||||
|
ax.grid(True, alpha=0.3, linestyle='--', axis='x')
|
||||||
|
else:
|
||||||
|
ax.set_xticks(positions)
|
||||||
|
ax.set_xticklabels(labels, rotation=45, ha='right')
|
||||||
|
ax.grid(True, alpha=0.3, linestyle='--', axis='y')
|
||||||
|
|
||||||
# 显示图例
|
# 显示图例
|
||||||
if len(datasets) > 1:
|
if len(datasets) > 1:
|
||||||
ax.legend(loc='best', framealpha=0.9)
|
ax.legend(loc='best', framealpha=0.9)
|
||||||
|
|
||||||
# 网格
|
|
||||||
ax.grid(True, alpha=0.3, linestyle='--', axis='y')
|
|
||||||
|
|
||||||
return self._figure_to_svg(fig)
|
return self._figure_to_svg(fig)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"渲染柱状图失败: {e}")
|
logger.error(f"渲染柱状图失败: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _render_bubble(
|
||||||
|
self,
|
||||||
|
data: Dict[str, Any],
|
||||||
|
props: Dict[str, Any],
|
||||||
|
width: int,
|
||||||
|
height: int,
|
||||||
|
dpi: int
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""渲染气泡图"""
|
||||||
|
try:
|
||||||
|
datasets = data.get('datasets', [])
|
||||||
|
if not datasets:
|
||||||
|
return None
|
||||||
|
|
||||||
|
title = props.get('title')
|
||||||
|
fig, ax = self._create_figure(width, height, dpi, title)
|
||||||
|
colors = self._get_colors(datasets)
|
||||||
|
|
||||||
|
def _safe_radius(raw) -> float:
|
||||||
|
try:
|
||||||
|
val = float(raw)
|
||||||
|
return max(val, 0.5)
|
||||||
|
except Exception:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
all_x: list[float] = []
|
||||||
|
all_y: list[float] = []
|
||||||
|
max_r: float = 0.0
|
||||||
|
|
||||||
|
for i, dataset in enumerate(datasets):
|
||||||
|
points = dataset.get('data', [])
|
||||||
|
label = dataset.get('label', f'系列{i+1}')
|
||||||
|
color = colors[i]
|
||||||
|
|
||||||
|
if points and isinstance(points[0], dict):
|
||||||
|
xs = [p.get('x', 0) for p in points]
|
||||||
|
ys = [p.get('y', 0) for p in points]
|
||||||
|
rs = [_safe_radius(p.get('r', 1)) for p in points]
|
||||||
|
else:
|
||||||
|
xs = list(range(len(points)))
|
||||||
|
ys = points
|
||||||
|
rs = [1.0 for _ in points]
|
||||||
|
|
||||||
|
all_x.extend(xs)
|
||||||
|
all_y.extend(ys)
|
||||||
|
if rs:
|
||||||
|
max_r = max(max_r, max(rs))
|
||||||
|
|
||||||
|
# 适度放大半径,近似Chart.js像素尺寸(动态尺度,避免过大遮挡)
|
||||||
|
size_scale = 8.0 if max_r <= 20 else 6.5
|
||||||
|
sizes = [(r * size_scale) ** 2 for r in rs]
|
||||||
|
|
||||||
|
ax.scatter(
|
||||||
|
xs,
|
||||||
|
ys,
|
||||||
|
s=sizes,
|
||||||
|
label=label,
|
||||||
|
color=color,
|
||||||
|
alpha=0.45,
|
||||||
|
edgecolors='white',
|
||||||
|
linewidth=0.6
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(datasets) > 1:
|
||||||
|
ax.legend(loc='best', framealpha=0.9)
|
||||||
|
|
||||||
|
# 适度留白,避免大气泡被裁切
|
||||||
|
if all_x and all_y:
|
||||||
|
x_min, x_max = min(all_x), max(all_x)
|
||||||
|
y_min, y_max = min(all_y), max(all_y)
|
||||||
|
x_span = max(x_max - x_min, 1e-6)
|
||||||
|
y_span = max(y_max - y_min, 1e-6)
|
||||||
|
pad_x = max(x_span * 0.12, max_r * 1.2)
|
||||||
|
pad_y = max(y_span * 0.12, max_r * 1.2)
|
||||||
|
ax.set_xlim(x_min - pad_x, x_max + pad_x)
|
||||||
|
ax.set_ylim(y_min - pad_y, y_max + pad_y)
|
||||||
|
# 额外安全边距
|
||||||
|
ax.margins(x=0.05, y=0.05)
|
||||||
|
|
||||||
|
ax.grid(True, alpha=0.3, linestyle='--')
|
||||||
|
return self._figure_to_svg(fig)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"渲染气泡图失败: {e}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
def _render_pie(
|
def _render_pie(
|
||||||
self,
|
self,
|
||||||
data: Dict[str, Any],
|
data: Dict[str, Any],
|
||||||
|
|||||||
@@ -1263,7 +1263,9 @@ class HTMLRenderer:
|
|||||||
def _render_math(self, block: Dict[str, Any]) -> str:
|
def _render_math(self, block: Dict[str, Any]) -> str:
|
||||||
"""渲染数学公式,占位符交给外部MathJax或后处理"""
|
"""渲染数学公式,占位符交给外部MathJax或后处理"""
|
||||||
latex = self._escape_html(block.get("latex", ""))
|
latex = self._escape_html(block.get("latex", ""))
|
||||||
return f'<div class="math-block">$$ {latex} $$</div>'
|
math_id = self._escape_attr(block.get("mathId", "")) if block.get("mathId") else ""
|
||||||
|
id_attr = f' data-math-id="{math_id}"' if math_id else ""
|
||||||
|
return f'<div class="math-block"{id_attr}>$$ {latex} $$</div>'
|
||||||
|
|
||||||
def _render_figure(self, block: Dict[str, Any]) -> str:
|
def _render_figure(self, block: Dict[str, Any]) -> str:
|
||||||
"""根据新规范默认不渲染外部图片,改为友好提示"""
|
"""根据新规范默认不渲染外部图片,改为友好提示"""
|
||||||
@@ -2012,7 +2014,9 @@ class HTMLRenderer:
|
|||||||
latex = math_mark.get("value")
|
latex = math_mark.get("value")
|
||||||
if not isinstance(latex, str) or not latex.strip():
|
if not isinstance(latex, str) or not latex.strip():
|
||||||
latex = text_value
|
latex = text_value
|
||||||
return f'<span class="math-inline">\\( {self._escape_html(latex)} \\)</span>'
|
math_id = self._escape_attr(run.get("mathId", "")) if run.get("mathId") else ""
|
||||||
|
id_attr = f' data-math-id="{math_id}"' if math_id else ""
|
||||||
|
return f'<span class="math-inline"{id_attr}>\\( {self._escape_html(latex)} \\)</span>'
|
||||||
text = self._escape_html(text_value)
|
text = self._escape_html(text_value)
|
||||||
styles: List[str] = []
|
styles: List[str] = []
|
||||||
prefix: List[str] = []
|
prefix: List[str] = []
|
||||||
|
|||||||
@@ -535,6 +535,33 @@ class PDFRenderer:
|
|||||||
if block_counter is None:
|
if block_counter is None:
|
||||||
block_counter = [0]
|
block_counter = [0]
|
||||||
|
|
||||||
|
def _extract_inline_math_from_inlines(inlines: list):
|
||||||
|
"""从段落内联节点中提取数学公式"""
|
||||||
|
if not isinstance(inlines, list):
|
||||||
|
return
|
||||||
|
for run in inlines:
|
||||||
|
if not isinstance(run, dict):
|
||||||
|
continue
|
||||||
|
marks = run.get('marks') or []
|
||||||
|
math_mark = next((m for m in marks if m.get('type') == 'math'), None)
|
||||||
|
if not math_mark:
|
||||||
|
continue
|
||||||
|
latex = (math_mark.get('value') or run.get('text') or '').strip()
|
||||||
|
if not latex:
|
||||||
|
continue
|
||||||
|
block_counter[0] += 1
|
||||||
|
math_id = f"math-inline-{block_counter[0]}"
|
||||||
|
try:
|
||||||
|
svg_content = self.math_converter.convert_inline_to_svg(latex)
|
||||||
|
if svg_content:
|
||||||
|
svg_map[math_id] = svg_content
|
||||||
|
run['mathId'] = math_id
|
||||||
|
logger.debug(f"公式 {math_id} 转换为SVG成功")
|
||||||
|
else:
|
||||||
|
logger.warning(f"公式 {math_id} 转换为SVG失败: {latex[:50]}...")
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(f"转换内联公式 {latex[:50]}... 时出错: {exc}")
|
||||||
|
|
||||||
for block in blocks:
|
for block in blocks:
|
||||||
if not isinstance(block, dict):
|
if not isinstance(block, dict):
|
||||||
continue
|
continue
|
||||||
@@ -547,7 +574,6 @@ class PDFRenderer:
|
|||||||
if latex:
|
if latex:
|
||||||
block_counter[0] += 1
|
block_counter[0] += 1
|
||||||
math_id = f"math-block-{block_counter[0]}"
|
math_id = f"math-block-{block_counter[0]}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
svg_content = self.math_converter.convert_display_to_svg(latex)
|
svg_content = self.math_converter.convert_display_to_svg(latex)
|
||||||
if svg_content:
|
if svg_content:
|
||||||
@@ -559,6 +585,11 @@ class PDFRenderer:
|
|||||||
logger.warning(f"公式 {math_id} 转换为SVG失败: {latex[:50]}...")
|
logger.warning(f"公式 {math_id} 转换为SVG失败: {latex[:50]}...")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"转换公式 {latex[:50]}... 时出错: {e}")
|
logger.error(f"转换公式 {latex[:50]}... 时出错: {e}")
|
||||||
|
else:
|
||||||
|
# 提取段落、表格等内部的内联公式
|
||||||
|
inlines = block.get('inlines')
|
||||||
|
if inlines:
|
||||||
|
_extract_inline_math_from_inlines(inlines)
|
||||||
|
|
||||||
# 递归处理嵌套的blocks
|
# 递归处理嵌套的blocks
|
||||||
nested_blocks = block.get('blocks')
|
nested_blocks = block.get('blocks')
|
||||||
@@ -614,9 +645,8 @@ class PDFRenderer:
|
|||||||
# 创建SVG容器HTML
|
# 创建SVG容器HTML
|
||||||
svg_html = f'<div class="chart-svg-container">{svg_content}</div>'
|
svg_html = f'<div class="chart-svg-container">{svg_content}</div>'
|
||||||
|
|
||||||
# 查找包含此widgetId的配置脚本
|
# 查找包含此widgetId的配置脚本(限制在同一个</script>内,避免跨标签误配)
|
||||||
# 格式: <script type="application/json" id="chart-config-N">{"widgetId":"widget_id",...}</script>
|
config_pattern = rf'<script[^>]+id="([^"]+)"[^>]*>(?:(?!</script>).)*?"widgetId"\s*:\s*"{re.escape(widget_id)}"(?:(?!</script>).)*?</script>'
|
||||||
config_pattern = rf'<script[^>]+id="([^"]+)"[^>]*>\s*\{{[^}}]*"widgetId"\s*:\s*"{re.escape(widget_id)}"[^}}]*\}}'
|
|
||||||
match = re.search(config_pattern, html, re.DOTALL)
|
match = re.search(config_pattern, html, re.DOTALL)
|
||||||
|
|
||||||
if match:
|
if match:
|
||||||
@@ -627,8 +657,11 @@ class PDFRenderer:
|
|||||||
canvas_pattern = rf'<canvas[^>]+data-config-id="{re.escape(config_id)}"[^>]*></canvas>'
|
canvas_pattern = rf'<canvas[^>]+data-config-id="{re.escape(config_id)}"[^>]*></canvas>'
|
||||||
|
|
||||||
# 【修复】替换canvas为SVG,使用lambda避免反斜杠转义问题
|
# 【修复】替换canvas为SVG,使用lambda避免反斜杠转义问题
|
||||||
html = re.sub(canvas_pattern, lambda m: svg_html, html)
|
html, replaced = re.subn(canvas_pattern, lambda m: svg_html, html, count=1)
|
||||||
logger.debug(f"已替换图表 {widget_id} 的canvas为SVG")
|
if replaced:
|
||||||
|
logger.debug(f"已替换图表 {widget_id} 的canvas为SVG")
|
||||||
|
else:
|
||||||
|
logger.warning(f"未找到图表 {widget_id} 的canvas进行替换")
|
||||||
|
|
||||||
# 将对应fallback标记为隐藏,避免PDF中出现重复表格
|
# 将对应fallback标记为隐藏,避免PDF中出现重复表格
|
||||||
fallback_pattern = rf'<div class="chart-fallback"([^>]*data-widget-id="{re.escape(widget_id)}"[^>]*)>'
|
fallback_pattern = rf'<div class="chart-fallback"([^>]*data-widget-id="{re.escape(widget_id)}"[^>]*)>'
|
||||||
@@ -661,7 +694,7 @@ class PDFRenderer:
|
|||||||
f'</div>'
|
f'</div>'
|
||||||
)
|
)
|
||||||
|
|
||||||
config_pattern = rf'<script[^>]+id="([^"]+)"[^>]*>\s*\{{[^}}]*"widgetId"\s*:\s*"{re.escape(widget_id)}"[^}}]*\}}'
|
config_pattern = rf'<script[^>]+id="([^"]+)"[^>]*>(?:(?!</script>).)*?"widgetId"\s*:\s*"{re.escape(widget_id)}"(?:(?!</script>).)*?</script>'
|
||||||
match = re.search(config_pattern, html, re.DOTALL)
|
match = re.search(config_pattern, html, re.DOTALL)
|
||||||
if not match:
|
if not match:
|
||||||
logger.debug(f"未找到词云 {widget_id} 的配置脚本,跳过注入")
|
logger.debug(f"未找到词云 {widget_id} 的配置脚本,跳过注入")
|
||||||
@@ -670,8 +703,11 @@ class PDFRenderer:
|
|||||||
config_id = match.group(1)
|
config_id = match.group(1)
|
||||||
canvas_pattern = rf'<canvas[^>]+data-config-id="{re.escape(config_id)}"[^>]*></canvas>'
|
canvas_pattern = rf'<canvas[^>]+data-config-id="{re.escape(config_id)}"[^>]*></canvas>'
|
||||||
|
|
||||||
html = re.sub(canvas_pattern, lambda m: img_html, html)
|
html, replaced = re.subn(canvas_pattern, lambda m: img_html, html, count=1)
|
||||||
logger.debug(f"已替换词云 {widget_id} 的canvas为PNG图片")
|
if replaced:
|
||||||
|
logger.debug(f"已替换词云 {widget_id} 的canvas为PNG图片")
|
||||||
|
else:
|
||||||
|
logger.warning(f"未找到词云 {widget_id} 的canvas进行替换")
|
||||||
|
|
||||||
fallback_pattern = rf'<div class="chart-fallback"([^>]*data-widget-id="{re.escape(widget_id)}"[^>]*)>'
|
fallback_pattern = rf'<div class="chart-fallback"([^>]*data-widget-id="{re.escape(widget_id)}"[^>]*)>'
|
||||||
|
|
||||||
@@ -701,32 +737,40 @@ class PDFRenderer:
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
# 为每个math block查找对应的div并替换为SVG
|
# 优先替换内联公式,再替换块级公式,保持顺序一致
|
||||||
for math_id, svg_content in svg_map.items():
|
for math_id, svg_content in svg_map.items():
|
||||||
# 清理SVG内容(移除XML声明,因为SVG将嵌入HTML)
|
# 清理SVG内容(移除XML声明,因为SVG将嵌入HTML)
|
||||||
svg_content = re.sub(r'<\?xml[^>]+\?>', '', svg_content)
|
svg_content = re.sub(r'<\?xml[^>]+\?>', '', svg_content)
|
||||||
svg_content = re.sub(r'<!DOCTYPE[^>]+>', '', svg_content)
|
svg_content = re.sub(r'<!DOCTYPE[^>]+>', '', svg_content)
|
||||||
svg_content = svg_content.strip()
|
svg_content = svg_content.strip()
|
||||||
|
|
||||||
# 创建SVG容器HTML
|
svg_block_html = f'<div class="math-svg-container">{svg_content}</div>'
|
||||||
svg_html = f'<div class="math-svg-container">{svg_content}</div>'
|
svg_inline_html = f'<span class="math-svg-inline">{svg_content}</span>'
|
||||||
|
|
||||||
# 查找对应的math-block div
|
replaced = False
|
||||||
# 格式: <div class="math-block">$$ latex $$</div>
|
# 优先按 data-math-id 精确替换
|
||||||
# 我们需要找到包含特定LaTeX内容的div
|
inline_pattern = rf'<span class="math-inline"[^>]*data-math-id="{re.escape(math_id)}"[^>]*>.*?</span>'
|
||||||
# 但由于我们在转换时已经给block添加了mathId,我们可以用另一种方式
|
if re.search(inline_pattern, html, re.DOTALL):
|
||||||
|
html = re.sub(inline_pattern, lambda m: svg_inline_html, html, count=1)
|
||||||
|
replaced = True
|
||||||
|
else:
|
||||||
|
block_pattern = rf'<div class="math-block"[^>]*data-math-id="{re.escape(math_id)}"[^>]*>.*?</div>'
|
||||||
|
if re.search(block_pattern, html, re.DOTALL):
|
||||||
|
html = re.sub(block_pattern, lambda m: svg_block_html, html, count=1)
|
||||||
|
replaced = True
|
||||||
|
|
||||||
# 方案:在HTML渲染器中为math-block添加data-math-id属性
|
# 如果没有找到特定ID,按出现顺序兜底替换
|
||||||
# 但这需要修改HTMLRenderer,暂时我们使用更简单的方法:
|
if not replaced:
|
||||||
# 按顺序替换所有math-block
|
html, sub_inline = re.subn(r'<span class="math-inline">[^<]*</span>', lambda m: svg_inline_html, html, count=1)
|
||||||
|
if sub_inline:
|
||||||
|
replaced = True
|
||||||
|
else:
|
||||||
|
html, sub_block = re.subn(r'<div class="math-block">\$\$[^$]*\$\$</div>', lambda m: svg_block_html, html, count=1)
|
||||||
|
if sub_block:
|
||||||
|
replaced = True
|
||||||
|
|
||||||
# 暂时使用简单的替换方案
|
if replaced:
|
||||||
# 找到第一个math-block div并替换
|
logger.debug(f"已替换公式 {math_id} 为SVG")
|
||||||
math_block_pattern = r'<div class="math-block">\$\$[^$]*\$\$</div>'
|
|
||||||
# 【修复】使用lambda函数避免re.sub将SVG内容中的反斜杠解释为转义序列
|
|
||||||
# lambda函数中的返回值会被当作字面字符串,不会进行转义处理
|
|
||||||
html = re.sub(math_block_pattern, lambda m: svg_html, html, count=1)
|
|
||||||
logger.debug(f"已替换公式 {math_id} 为SVG")
|
|
||||||
|
|
||||||
return html
|
return html
|
||||||
|
|
||||||
@@ -787,10 +831,8 @@ class PDFRenderer:
|
|||||||
logger.info("开始转换数学公式为SVG矢量图形...")
|
logger.info("开始转换数学公式为SVG矢量图形...")
|
||||||
math_svg_map = self._convert_math_to_svg(preprocessed_ir)
|
math_svg_map = self._convert_math_to_svg(preprocessed_ir)
|
||||||
|
|
||||||
# 使用HTML渲染器生成基础HTML(使用原始IR,因为HTMLRenderer会自己修复)
|
# 使用HTML渲染器生成基础HTML(使用预处理后的IR,以便复用mathId等标记)
|
||||||
# 注意:这里仍使用原始document_ir,因为HTMLRenderer内部会进行相同的修复
|
html = self.html_renderer.render(preprocessed_ir)
|
||||||
# 这确保了HTML和SVG使用相同的修复逻辑
|
|
||||||
html = self.html_renderer.render(document_ir)
|
|
||||||
|
|
||||||
# 注入图表SVG
|
# 注入图表SVG
|
||||||
if svg_map:
|
if svg_map:
|
||||||
|
|||||||
Reference in New Issue
Block a user