Added Support for Formulas and Optimize the Rendering of Data Blocks When Exporting to PDF
Showing
3 changed files
with
400 additions
and
1 deletions
ReportEngine/renderers/math_to_svg.py
0 → 100644
| 1 | +""" | ||
| 2 | +LaTeX 数学公式转 SVG 渲染器 | ||
| 3 | +使用 matplotlib 将 LaTeX 公式渲染为 SVG 格式,用于 PDF 导出 | ||
| 4 | +""" | ||
| 5 | + | ||
| 6 | +import io | ||
| 7 | +import logging | ||
| 8 | +from typing import Optional | ||
| 9 | +import matplotlib | ||
| 10 | +import matplotlib.pyplot as plt | ||
| 11 | +from matplotlib import mathtext | ||
| 12 | + | ||
| 13 | +# 使用非交互式后端 | ||
| 14 | +matplotlib.use('Agg') | ||
| 15 | + | ||
| 16 | +logger = logging.getLogger(__name__) | ||
| 17 | + | ||
| 18 | + | ||
| 19 | +class MathToSVG: | ||
| 20 | + """将 LaTeX 数学公式转换为 SVG 的转换器""" | ||
| 21 | + | ||
| 22 | + def __init__(self, font_size: int = 14, color: str = 'black'): | ||
| 23 | + """ | ||
| 24 | + 初始化公式转换器 | ||
| 25 | + | ||
| 26 | + Args: | ||
| 27 | + font_size: 字体大小(点) | ||
| 28 | + color: 文字颜色 | ||
| 29 | + """ | ||
| 30 | + self.font_size = font_size | ||
| 31 | + self.color = color | ||
| 32 | + | ||
| 33 | + def convert_to_svg(self, latex: str, display_mode: bool = True) -> Optional[str]: | ||
| 34 | + """ | ||
| 35 | + 将 LaTeX 公式转换为 SVG 字符串 | ||
| 36 | + | ||
| 37 | + Args: | ||
| 38 | + latex: LaTeX 公式字符串(不包含 $$ 或 $ 符号) | ||
| 39 | + display_mode: True 为显示模式(块级公式),False 为行内模式 | ||
| 40 | + | ||
| 41 | + Returns: | ||
| 42 | + SVG 字符串,如果转换失败则返回 None | ||
| 43 | + """ | ||
| 44 | + try: | ||
| 45 | + # 清理 LaTeX 字符串 | ||
| 46 | + latex = latex.strip() | ||
| 47 | + if not latex: | ||
| 48 | + logger.warning("空的 LaTeX 公式") | ||
| 49 | + return None | ||
| 50 | + | ||
| 51 | + # 创建图形 | ||
| 52 | + fig = plt.figure(figsize=(10, 2) if display_mode else (6, 1)) | ||
| 53 | + fig.patch.set_alpha(0) # 透明背景 | ||
| 54 | + | ||
| 55 | + # 渲染 LaTeX | ||
| 56 | + # 使用 mathtext 进行渲染 | ||
| 57 | + if display_mode: | ||
| 58 | + # 显示模式:居中,较大字体 | ||
| 59 | + text = fig.text( | ||
| 60 | + 0.5, 0.5, | ||
| 61 | + f'${latex}$', | ||
| 62 | + fontsize=self.font_size * 1.2, | ||
| 63 | + color=self.color, | ||
| 64 | + ha='center', | ||
| 65 | + va='center', | ||
| 66 | + usetex=False # 使用 matplotlib 内置的 mathtext 而非完整 LaTeX | ||
| 67 | + ) | ||
| 68 | + else: | ||
| 69 | + # 行内模式:左对齐,正常字体 | ||
| 70 | + text = fig.text( | ||
| 71 | + 0.1, 0.5, | ||
| 72 | + f'${latex}$', | ||
| 73 | + fontsize=self.font_size, | ||
| 74 | + color=self.color, | ||
| 75 | + ha='left', | ||
| 76 | + va='center', | ||
| 77 | + usetex=False | ||
| 78 | + ) | ||
| 79 | + | ||
| 80 | + # 获取文本边界框 | ||
| 81 | + fig.canvas.draw() | ||
| 82 | + bbox = text.get_window_extent(renderer=fig.canvas.get_renderer()) | ||
| 83 | + | ||
| 84 | + # 转换为英寸(matplotlib 使用的单位) | ||
| 85 | + bbox_inches = bbox.transformed(fig.dpi_scale_trans.inverted()) | ||
| 86 | + | ||
| 87 | + # 调整图形大小以适应文本,添加边距 | ||
| 88 | + margin = 0.1 # 英寸 | ||
| 89 | + fig.set_size_inches( | ||
| 90 | + bbox_inches.width + 2 * margin, | ||
| 91 | + bbox_inches.height + 2 * margin | ||
| 92 | + ) | ||
| 93 | + | ||
| 94 | + # 重新定位文本到中心 | ||
| 95 | + text.set_position((0.5, 0.5)) | ||
| 96 | + | ||
| 97 | + # 保存为 SVG | ||
| 98 | + svg_buffer = io.StringIO() | ||
| 99 | + plt.savefig( | ||
| 100 | + svg_buffer, | ||
| 101 | + format='svg', | ||
| 102 | + bbox_inches='tight', | ||
| 103 | + pad_inches=0.1, | ||
| 104 | + transparent=True, | ||
| 105 | + dpi=300 | ||
| 106 | + ) | ||
| 107 | + plt.close(fig) | ||
| 108 | + | ||
| 109 | + # 获取 SVG 内容 | ||
| 110 | + svg_content = svg_buffer.getvalue() | ||
| 111 | + svg_buffer.close() | ||
| 112 | + | ||
| 113 | + return svg_content | ||
| 114 | + | ||
| 115 | + except Exception as e: | ||
| 116 | + logger.error(f"LaTeX 公式转换失败: {latex[:100]}... 错误: {str(e)}") | ||
| 117 | + return None | ||
| 118 | + | ||
| 119 | + def convert_inline_to_svg(self, latex: str) -> Optional[str]: | ||
| 120 | + """ | ||
| 121 | + 将行内 LaTeX 公式转换为 SVG | ||
| 122 | + | ||
| 123 | + Args: | ||
| 124 | + latex: LaTeX 公式字符串 | ||
| 125 | + | ||
| 126 | + Returns: | ||
| 127 | + SVG 字符串,如果转换失败则返回 None | ||
| 128 | + """ | ||
| 129 | + return self.convert_to_svg(latex, display_mode=False) | ||
| 130 | + | ||
| 131 | + def convert_display_to_svg(self, latex: str) -> Optional[str]: | ||
| 132 | + """ | ||
| 133 | + 将显示模式 LaTeX 公式转换为 SVG | ||
| 134 | + | ||
| 135 | + Args: | ||
| 136 | + latex: LaTeX 公式字符串 | ||
| 137 | + | ||
| 138 | + Returns: | ||
| 139 | + SVG 字符串,如果转换失败则返回 None | ||
| 140 | + """ | ||
| 141 | + return self.convert_to_svg(latex, display_mode=True) | ||
| 142 | + | ||
| 143 | + | ||
| 144 | +def convert_math_block_to_svg( | ||
| 145 | + latex: str, | ||
| 146 | + font_size: int = 16, | ||
| 147 | + color: str = 'black' | ||
| 148 | +) -> Optional[str]: | ||
| 149 | + """ | ||
| 150 | + 便捷函数:将数学公式块转换为 SVG | ||
| 151 | + | ||
| 152 | + Args: | ||
| 153 | + latex: LaTeX 公式字符串 | ||
| 154 | + font_size: 字体大小 | ||
| 155 | + color: 文字颜色 | ||
| 156 | + | ||
| 157 | + Returns: | ||
| 158 | + SVG 字符串,如果转换失败则返回 None | ||
| 159 | + """ | ||
| 160 | + converter = MathToSVG(font_size=font_size, color=color) | ||
| 161 | + return converter.convert_display_to_svg(latex) | ||
| 162 | + | ||
| 163 | + | ||
| 164 | +def convert_math_inline_to_svg( | ||
| 165 | + latex: str, | ||
| 166 | + font_size: int = 14, | ||
| 167 | + color: str = 'black' | ||
| 168 | +) -> Optional[str]: | ||
| 169 | + """ | ||
| 170 | + 便捷函数:将行内数学公式转换为 SVG | ||
| 171 | + | ||
| 172 | + Args: | ||
| 173 | + latex: LaTeX 公式字符串 | ||
| 174 | + font_size: 字体大小 | ||
| 175 | + color: 文字颜色 | ||
| 176 | + | ||
| 177 | + Returns: | ||
| 178 | + SVG 字符串,如果转换失败则返回 None | ||
| 179 | + """ | ||
| 180 | + converter = MathToSVG(font_size=font_size, color=color) | ||
| 181 | + return converter.convert_inline_to_svg(latex) | ||
| 182 | + | ||
| 183 | + | ||
| 184 | +if __name__ == "__main__": | ||
| 185 | + # 测试代码 | ||
| 186 | + import sys | ||
| 187 | + | ||
| 188 | + # 配置日志 | ||
| 189 | + logging.basicConfig( | ||
| 190 | + level=logging.INFO, | ||
| 191 | + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' | ||
| 192 | + ) | ||
| 193 | + | ||
| 194 | + # 测试公式 | ||
| 195 | + test_formulas = [ | ||
| 196 | + r"E = mc^2", | ||
| 197 | + r"\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}", | ||
| 198 | + r"\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}", | ||
| 199 | + r"\sum_{i=1}^{n} i = \frac{n(n+1)}{2}", | ||
| 200 | + ] | ||
| 201 | + | ||
| 202 | + converter = MathToSVG(font_size=16) | ||
| 203 | + | ||
| 204 | + for i, formula in enumerate(test_formulas): | ||
| 205 | + logger.info(f"测试公式 {i+1}: {formula}") | ||
| 206 | + svg = converter.convert_display_to_svg(formula) | ||
| 207 | + if svg: | ||
| 208 | + # 保存到文件 | ||
| 209 | + filename = f"test_math_{i+1}.svg" | ||
| 210 | + with open(filename, 'w', encoding='utf-8') as f: | ||
| 211 | + f.write(svg) | ||
| 212 | + logger.info(f"成功保存到 {filename}") | ||
| 213 | + else: | ||
| 214 | + logger.error(f"公式 {i+1} 转换失败") | ||
| 215 | + | ||
| 216 | + logger.info("测试完成") |
| @@ -805,6 +805,13 @@ p {{ | @@ -805,6 +805,13 @@ p {{ | ||
| 805 | line-height: {cfg.callout.line_height}; | 805 | line-height: {cfg.callout.line_height}; |
| 806 | }} | 806 | }} |
| 807 | 807 | ||
| 808 | +/* 确保 callout 内部最后一个元素不会溢出底部 */ | ||
| 809 | +.callout > *:last-child, | ||
| 810 | +.callout > *:last-child > *:last-child {{ | ||
| 811 | + margin-bottom: 0 !important; | ||
| 812 | + padding-bottom: 0 !important; | ||
| 813 | +}} | ||
| 814 | + | ||
| 808 | /* 表格优化 - 严格防止溢出 */ | 815 | /* 表格优化 - 严格防止溢出 */ |
| 809 | table {{ | 816 | table {{ |
| 810 | width: 100%; | 817 | width: 100%; |
| @@ -33,6 +33,7 @@ except Exception as e: | @@ -33,6 +33,7 @@ except Exception as e: | ||
| 33 | from .html_renderer import HTMLRenderer | 33 | from .html_renderer import HTMLRenderer |
| 34 | from .pdf_layout_optimizer import PDFLayoutOptimizer, PDFLayoutConfig | 34 | from .pdf_layout_optimizer import PDFLayoutOptimizer, PDFLayoutConfig |
| 35 | from .chart_to_svg import create_chart_converter | 35 | from .chart_to_svg import create_chart_converter |
| 36 | +from .math_to_svg import MathToSVG | ||
| 36 | 37 | ||
| 37 | 38 | ||
| 38 | class PDFRenderer: | 39 | class PDFRenderer: |
| @@ -71,6 +72,14 @@ class PDFRenderer: | @@ -71,6 +72,14 @@ class PDFRenderer: | ||
| 71 | except Exception as e: | 72 | except Exception as e: |
| 72 | logger.warning(f"图表SVG转换器初始化失败: {e},将使用表格降级") | 73 | logger.warning(f"图表SVG转换器初始化失败: {e},将使用表格降级") |
| 73 | 74 | ||
| 75 | + # 初始化数学公式转换器 | ||
| 76 | + try: | ||
| 77 | + self.math_converter = MathToSVG(font_size=16, color='black') | ||
| 78 | + logger.info("数学公式SVG转换器初始化成功") | ||
| 79 | + except Exception as e: | ||
| 80 | + logger.warning(f"数学公式SVG转换器初始化失败: {e},公式将显示为文本") | ||
| 81 | + self.math_converter = None | ||
| 82 | + | ||
| 74 | @staticmethod | 83 | @staticmethod |
| 75 | def _get_font_path() -> Path: | 84 | def _get_font_path() -> Path: |
| 76 | """获取字体文件路径""" | 85 | """获取字体文件路径""" |
| @@ -280,6 +289,101 @@ class PDFRenderer: | @@ -280,6 +289,101 @@ class PDFRenderer: | ||
| 280 | if isinstance(cell_blocks, list): | 289 | if isinstance(cell_blocks, list): |
| 281 | self._extract_and_convert_widgets(cell_blocks, svg_map) | 290 | self._extract_and_convert_widgets(cell_blocks, svg_map) |
| 282 | 291 | ||
| 292 | + def _convert_math_to_svg(self, document_ir: Dict[str, Any]) -> Dict[str, str]: | ||
| 293 | + """ | ||
| 294 | + 将document_ir中的所有数学公式转换为SVG | ||
| 295 | + | ||
| 296 | + 参数: | ||
| 297 | + document_ir: Document IR数据 | ||
| 298 | + | ||
| 299 | + 返回: | ||
| 300 | + Dict[str, str]: 公式块ID到SVG字符串的映射 | ||
| 301 | + """ | ||
| 302 | + svg_map = {} | ||
| 303 | + | ||
| 304 | + if not hasattr(self, 'math_converter') or not self.math_converter: | ||
| 305 | + logger.warning("数学公式转换器未初始化,跳过公式转换") | ||
| 306 | + return svg_map | ||
| 307 | + | ||
| 308 | + # 遍历所有章节 | ||
| 309 | + chapters = document_ir.get('chapters', []) | ||
| 310 | + for chapter in chapters: | ||
| 311 | + blocks = chapter.get('blocks', []) | ||
| 312 | + self._extract_and_convert_math_blocks(blocks, svg_map) | ||
| 313 | + | ||
| 314 | + logger.info(f"成功转换 {len(svg_map)} 个数学公式为SVG") | ||
| 315 | + return svg_map | ||
| 316 | + | ||
| 317 | + def _extract_and_convert_math_blocks( | ||
| 318 | + self, | ||
| 319 | + blocks: list, | ||
| 320 | + svg_map: Dict[str, str], | ||
| 321 | + block_counter: list = None | ||
| 322 | + ) -> None: | ||
| 323 | + """ | ||
| 324 | + 递归遍历blocks,找到所有math块并转换为SVG | ||
| 325 | + | ||
| 326 | + 参数: | ||
| 327 | + blocks: block列表 | ||
| 328 | + svg_map: 用于存储转换结果的字典 | ||
| 329 | + block_counter: 用于生成唯一ID的计数器 | ||
| 330 | + """ | ||
| 331 | + if block_counter is None: | ||
| 332 | + block_counter = [0] | ||
| 333 | + | ||
| 334 | + for block in blocks: | ||
| 335 | + if not isinstance(block, dict): | ||
| 336 | + continue | ||
| 337 | + | ||
| 338 | + block_type = block.get('type') | ||
| 339 | + | ||
| 340 | + # 处理math类型 | ||
| 341 | + if block_type == 'math': | ||
| 342 | + latex = block.get('latex', '').strip() | ||
| 343 | + if latex: | ||
| 344 | + block_counter[0] += 1 | ||
| 345 | + math_id = f"math-block-{block_counter[0]}" | ||
| 346 | + | ||
| 347 | + try: | ||
| 348 | + svg_content = self.math_converter.convert_display_to_svg(latex) | ||
| 349 | + if svg_content: | ||
| 350 | + svg_map[math_id] = svg_content | ||
| 351 | + # 将ID添加到block中,以便后续注入时识别 | ||
| 352 | + block['mathId'] = math_id | ||
| 353 | + logger.debug(f"公式 {math_id} 转换为SVG成功") | ||
| 354 | + else: | ||
| 355 | + logger.warning(f"公式 {math_id} 转换为SVG失败: {latex[:50]}...") | ||
| 356 | + except Exception as e: | ||
| 357 | + logger.error(f"转换公式 {latex[:50]}... 时出错: {e}") | ||
| 358 | + | ||
| 359 | + # 递归处理嵌套的blocks | ||
| 360 | + nested_blocks = block.get('blocks') | ||
| 361 | + if isinstance(nested_blocks, list): | ||
| 362 | + self._extract_and_convert_math_blocks(nested_blocks, svg_map, block_counter) | ||
| 363 | + | ||
| 364 | + # 处理列表项 | ||
| 365 | + if block_type == 'list': | ||
| 366 | + items = block.get('items', []) | ||
| 367 | + for item in items: | ||
| 368 | + if isinstance(item, list): | ||
| 369 | + self._extract_and_convert_math_blocks(item, svg_map, block_counter) | ||
| 370 | + | ||
| 371 | + # 处理表格单元格 | ||
| 372 | + if block_type == 'table': | ||
| 373 | + rows = block.get('rows', []) | ||
| 374 | + for row in rows: | ||
| 375 | + cells = row.get('cells', []) | ||
| 376 | + for cell in cells: | ||
| 377 | + cell_blocks = cell.get('blocks', []) | ||
| 378 | + if isinstance(cell_blocks, list): | ||
| 379 | + self._extract_and_convert_math_blocks(cell_blocks, svg_map, block_counter) | ||
| 380 | + | ||
| 381 | + # 处理callout内部的blocks | ||
| 382 | + if block_type == 'callout': | ||
| 383 | + callout_blocks = block.get('blocks', []) | ||
| 384 | + if isinstance(callout_blocks, list): | ||
| 385 | + self._extract_and_convert_math_blocks(callout_blocks, svg_map, block_counter) | ||
| 386 | + | ||
| 283 | def _inject_svg_into_html(self, html: str, svg_map: Dict[str, str]) -> str: | 387 | def _inject_svg_into_html(self, html: str, svg_map: Dict[str, str]) -> str: |
| 284 | """ | 388 | """ |
| 285 | 将SVG内容直接注入到HTML中(不使用JavaScript) | 389 | 将SVG内容直接注入到HTML中(不使用JavaScript) |
| @@ -326,6 +430,49 @@ class PDFRenderer: | @@ -326,6 +430,49 @@ class PDFRenderer: | ||
| 326 | 430 | ||
| 327 | return html | 431 | return html |
| 328 | 432 | ||
| 433 | + def _inject_math_svg_into_html(self, html: str, svg_map: Dict[str, str]) -> str: | ||
| 434 | + """ | ||
| 435 | + 将数学公式SVG内容注入到HTML中 | ||
| 436 | + | ||
| 437 | + 参数: | ||
| 438 | + html: 原始HTML内容 | ||
| 439 | + svg_map: 公式ID到SVG内容的映射 | ||
| 440 | + | ||
| 441 | + 返回: | ||
| 442 | + str: 注入SVG后的HTML | ||
| 443 | + """ | ||
| 444 | + if not svg_map: | ||
| 445 | + return html | ||
| 446 | + | ||
| 447 | + import re | ||
| 448 | + | ||
| 449 | + # 为每个math block查找对应的div并替换为SVG | ||
| 450 | + for math_id, svg_content in svg_map.items(): | ||
| 451 | + # 清理SVG内容(移除XML声明,因为SVG将嵌入HTML) | ||
| 452 | + svg_content = re.sub(r'<\?xml[^>]+\?>', '', svg_content) | ||
| 453 | + svg_content = re.sub(r'<!DOCTYPE[^>]+>', '', svg_content) | ||
| 454 | + svg_content = svg_content.strip() | ||
| 455 | + | ||
| 456 | + # 创建SVG容器HTML | ||
| 457 | + svg_html = f'<div class="math-svg-container">{svg_content}</div>' | ||
| 458 | + | ||
| 459 | + # 查找对应的math-block div | ||
| 460 | + # 格式: <div class="math-block">$$ latex $$</div> | ||
| 461 | + # 我们需要找到包含特定LaTeX内容的div | ||
| 462 | + # 但由于我们在转换时已经给block添加了mathId,我们可以用另一种方式 | ||
| 463 | + | ||
| 464 | + # 方案:在HTML渲染器中为math-block添加data-math-id属性 | ||
| 465 | + # 但这需要修改HTMLRenderer,暂时我们使用更简单的方法: | ||
| 466 | + # 按顺序替换所有math-block | ||
| 467 | + | ||
| 468 | + # 暂时使用简单的替换方案 | ||
| 469 | + # 找到第一个math-block div并替换 | ||
| 470 | + math_block_pattern = r'<div class="math-block">\$\$[^$]*\$\$</div>' | ||
| 471 | + html = re.sub(math_block_pattern, svg_html, html, count=1) | ||
| 472 | + logger.debug(f"已替换公式 {math_id} 为SVG") | ||
| 473 | + | ||
| 474 | + return html | ||
| 475 | + | ||
| 329 | def _get_pdf_html( | 476 | def _get_pdf_html( |
| 330 | self, | 477 | self, |
| 331 | document_ir: Dict[str, Any], | 478 | document_ir: Dict[str, Any], |
| @@ -375,16 +522,25 @@ class PDFRenderer: | @@ -375,16 +522,25 @@ class PDFRenderer: | ||
| 375 | logger.info("开始转换图表为SVG矢量图形...") | 522 | logger.info("开始转换图表为SVG矢量图形...") |
| 376 | svg_map = self._convert_charts_to_svg(preprocessed_ir) | 523 | svg_map = self._convert_charts_to_svg(preprocessed_ir) |
| 377 | 524 | ||
| 525 | + # 转换数学公式为SVG | ||
| 526 | + logger.info("开始转换数学公式为SVG矢量图形...") | ||
| 527 | + math_svg_map = self._convert_math_to_svg(preprocessed_ir) | ||
| 528 | + | ||
| 378 | # 使用HTML渲染器生成基础HTML(使用原始IR,因为HTMLRenderer会自己修复) | 529 | # 使用HTML渲染器生成基础HTML(使用原始IR,因为HTMLRenderer会自己修复) |
| 379 | # 注意:这里仍使用原始document_ir,因为HTMLRenderer内部会进行相同的修复 | 530 | # 注意:这里仍使用原始document_ir,因为HTMLRenderer内部会进行相同的修复 |
| 380 | # 这确保了HTML和SVG使用相同的修复逻辑 | 531 | # 这确保了HTML和SVG使用相同的修复逻辑 |
| 381 | html = self.html_renderer.render(document_ir) | 532 | html = self.html_renderer.render(document_ir) |
| 382 | 533 | ||
| 383 | - # 注入SVG | 534 | + # 注入图表SVG |
| 384 | if svg_map: | 535 | if svg_map: |
| 385 | html = self._inject_svg_into_html(html, svg_map) | 536 | html = self._inject_svg_into_html(html, svg_map) |
| 386 | logger.info(f"已注入 {len(svg_map)} 个SVG图表") | 537 | logger.info(f"已注入 {len(svg_map)} 个SVG图表") |
| 387 | 538 | ||
| 539 | + # 注入数学公式SVG | ||
| 540 | + if math_svg_map: | ||
| 541 | + html = self._inject_math_svg_into_html(html, math_svg_map) | ||
| 542 | + logger.info(f"已注入 {len(math_svg_map)} 个SVG公式") | ||
| 543 | + | ||
| 388 | # 获取字体路径并转换为base64(用于嵌入) | 544 | # 获取字体路径并转换为base64(用于嵌入) |
| 389 | font_path = self._get_font_path() | 545 | font_path = self._get_font_path() |
| 390 | font_data = font_path.read_bytes() | 546 | font_data = font_path.read_bytes() |
| @@ -439,6 +595,26 @@ body {{ | @@ -439,6 +595,26 @@ body {{ | ||
| 439 | height: auto; | 595 | height: auto; |
| 440 | }} | 596 | }} |
| 441 | 597 | ||
| 598 | +/* 数学公式SVG容器样式 */ | ||
| 599 | +.math-svg-container {{ | ||
| 600 | + width: 100%; | ||
| 601 | + height: auto; | ||
| 602 | + display: flex; | ||
| 603 | + justify-content: center; | ||
| 604 | + align-items: center; | ||
| 605 | + margin: 20px 0; | ||
| 606 | +}} | ||
| 607 | + | ||
| 608 | +.math-svg-container svg {{ | ||
| 609 | + max-width: 100%; | ||
| 610 | + height: auto; | ||
| 611 | +}} | ||
| 612 | + | ||
| 613 | +/* 隐藏原始的math-block(因为已被SVG替换) */ | ||
| 614 | +.math-block {{ | ||
| 615 | + display: none !important; | ||
| 616 | +}} | ||
| 617 | + | ||
| 442 | /* 隐藏fallback表格(因为现在使用SVG) */ | 618 | /* 隐藏fallback表格(因为现在使用SVG) */ |
| 443 | .chart-fallback {{ | 619 | .chart-fallback {{ |
| 444 | display: none !important; | 620 | display: none !important; |
-
Please register or login to post a comment