服务:Spring Boot 3.2 + docx4j 8.3.9,POST /api/v3/server_office_file/file/parse
接受 .docx,返回 HTML(公式 → LaTeX,图片 → RustFS 外链)
公式支持三类来源:
- OMML(Word 2007+ 公式) → Python omml_mathml2latex
- MathType OLE 对象 → Ruby mathtype2mathml → MathML → LaTeX
- Word EQ 域(老教辅主力) → EqToLatexUtil(本次新增)
本次改动:
1. 新增 java-svc/.../core/EqToLatexUtil.java:
移植自 jby backend-services office-core,EQ 语法 → LaTeX
(\f→\dfrac、\r→\sqrt、\i→\int/\sum/\prod、\s→^{}/_{}、\b→\left\right、
\a→array、\x→\boxed/\overline、\o\al 堆叠、圈码、化学符号、极限、连等式)
品牌标记 JBYU* → WPU*,与采集端一致
2. CtoMathFinder:接上 EQ 分支(原为 not-ported 桩),域代码经
EqToLatexUtil.convert 转 LaTeX 写入 eqLatexMap(WPEQNUM 占位符)
3. WordParseUtil.dealPlaceholder:EQ 的 LaTeX 同样加 \( \) 包裹
(原先因 WPEQNUM 例外不加包裹,前端/MathJax 不渲染)
修前症状:教辅 Word 里分式/方程组/上下标整段丢失(如 {−1/3, 1} 变成 {-,1})
修后实测:46 份教辅 2908 处 EQ 域全部还原,公式总数 2954 处、零未包裹残留
安全:application.yml(含 RustFS 密钥)不入库,提供 application.yml.example;
OssService 的 @Value 默认密钥已清空
222 lines
8.2 KiB
Python
222 lines
8.2 KiB
Python
"""
|
|
Output formatting for conversion results.
|
|
|
|
This module provides functions to format conversion results in various output
|
|
formats including LaTeX, JSON, and HTML with MathJax rendering.
|
|
"""
|
|
|
|
import json
|
|
from typing import List
|
|
from .result import ConversionResult
|
|
from ..utils.text_processing import write_text
|
|
|
|
|
|
def format_latex(results: List[ConversionResult], format_type: str = "display",
|
|
delimiter: str = "dollar") -> str:
|
|
"""Format LaTeX results for output.
|
|
|
|
Args:
|
|
results: List of conversion results
|
|
format_type: 'inline', 'display', or 'both' (auto-detect)
|
|
delimiter: 'dollar', 'bracket', 'mathjax', or 'katex'
|
|
|
|
Returns:
|
|
Formatted LaTeX string with appropriate delimiters
|
|
|
|
Example:
|
|
>>> results = [ConversionResult()]
|
|
>>> results[0].success = True
|
|
>>> results[0].latex = 'x + y'
|
|
>>> results[0].display_mode = False
|
|
>>> output = format_latex(results, 'both', 'dollar')
|
|
>>> print(output)
|
|
'$x + y$'
|
|
"""
|
|
if delimiter == "dollar":
|
|
inline_start, inline_end = "$", "$"
|
|
display_start, display_end = "$$", "$$"
|
|
elif delimiter == "bracket":
|
|
inline_start, inline_end = "\\(", "\\)"
|
|
display_start, display_end = "\\[", "\\]"
|
|
elif delimiter == "mathjax":
|
|
inline_start, inline_end = "\\(", "\\)"
|
|
display_start, display_end = "$$", "$$"
|
|
elif delimiter == "katex":
|
|
inline_start, inline_end = "$", "$"
|
|
display_start, display_end = "$$", "$$"
|
|
else:
|
|
inline_start, inline_end = "$", "$"
|
|
display_start, display_end = "$$", "$$"
|
|
|
|
output_lines = []
|
|
|
|
for i, result in enumerate(results):
|
|
if not result.success:
|
|
output_lines.append(f"% Equation {i+1}: ERROR - {result.error}")
|
|
continue
|
|
|
|
# Determine if equation should be inline or display
|
|
if hasattr(result, 'display_mode') and format_type == "both":
|
|
# Use the detected display mode from OMML structure
|
|
if result.display_mode:
|
|
equation = f"{display_start}{result.latex}{display_end}"
|
|
else:
|
|
equation = f"{inline_start}{result.latex}{inline_end}"
|
|
elif format_type == "inline":
|
|
equation = f"{inline_start}{result.latex}{inline_end}"
|
|
elif format_type == "display":
|
|
equation = f"{display_start}{result.latex}{display_end}"
|
|
else: # format_type == "both" but no display_mode set
|
|
# Use heuristic: short equations inline, complex ones display
|
|
if len(result.latex) < 50 and not any(cmd in result.latex for cmd in ["\\frac", "\\sum", "\\int", "\\begin"]):
|
|
equation = f"{inline_start}{result.latex}{inline_end}"
|
|
else:
|
|
equation = f"{display_start}{result.latex}{display_end}"
|
|
|
|
output_lines.append(equation)
|
|
output_lines.append("") # Empty line between equations
|
|
|
|
return "\n".join(output_lines)
|
|
|
|
|
|
def save_results(results: List[ConversionResult], output_path: str,
|
|
format_type: str = "display", delimiter: str = "dollar",
|
|
include_context: bool = False, output_format: str = None):
|
|
"""Save conversion results in various formats.
|
|
|
|
Args:
|
|
results: List of conversion results
|
|
output_path: Output file path
|
|
format_type: For LaTeX output - 'inline', 'display', or 'both'
|
|
delimiter: For LaTeX output - delimiter style
|
|
include_context: Whether to include context in JSON output
|
|
output_format: 'latex', 'json', or 'html' (auto-detected from output_path if not provided)
|
|
|
|
Example:
|
|
>>> results = [ConversionResult()]
|
|
>>> save_results(results, 'output.json')
|
|
>>> save_results(results, 'output.tex', format_type='inline', delimiter='dollar')
|
|
"""
|
|
# Auto-detect output format from file extension if not provided
|
|
if output_format is None:
|
|
if output_path.endswith('.json'):
|
|
output_format = 'json'
|
|
elif output_path.endswith('.html'):
|
|
output_format = 'html'
|
|
else:
|
|
# Default to LaTeX for .tex, .md, and any other extension
|
|
output_format = 'latex'
|
|
|
|
if output_format == "latex":
|
|
latex_content = format_latex(results, format_type, delimiter)
|
|
write_text(output_path, latex_content)
|
|
|
|
elif output_format == "json":
|
|
json_data = {
|
|
"metadata": {
|
|
"total_equations": len(results),
|
|
"successful_conversions": sum(1 for r in results if r.success),
|
|
"failed_conversions": sum(1 for r in results if not r.success),
|
|
"format_type": format_type,
|
|
"delimiter": delimiter,
|
|
"include_context": include_context
|
|
},
|
|
"equations": [result.to_dict() for result in results]
|
|
}
|
|
|
|
with open(output_path, 'w', encoding='utf-8') as f:
|
|
json.dump(json_data, f, indent=2, ensure_ascii=False)
|
|
|
|
elif output_format == "html":
|
|
html_content = generate_html_preview(results, format_type, delimiter)
|
|
write_text(output_path, html_content)
|
|
|
|
|
|
def generate_html_preview(results: List[ConversionResult], format_type: str, delimiter: str) -> str:
|
|
"""Generate HTML preview with MathJax rendering.
|
|
|
|
Args:
|
|
results: List of conversion results
|
|
format_type: 'inline', 'display', or 'both'
|
|
delimiter: Delimiter style
|
|
|
|
Returns:
|
|
HTML document string with embedded MathJax
|
|
|
|
Example:
|
|
>>> results = [ConversionResult()]
|
|
>>> html = generate_html_preview(results, 'display', 'dollar')
|
|
>>> '<!DOCTYPE html>' in html
|
|
True
|
|
"""
|
|
html_template = """<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>OMML to LaTeX Conversion Results</title>
|
|
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
|
|
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
|
<style>
|
|
body {{ font-family: Arial, sans-serif; margin: 40px; }}
|
|
.equation {{ margin: 20px 0; padding: 15px; border: 1px solid #ddd; background: #f9f9f9; }}
|
|
.context {{ font-style: italic; color: #666; margin-bottom: 10px; }}
|
|
.latex-code {{ font-family: monospace; background: #f0f0f0; padding: 10px; margin: 5px 0; }}
|
|
.error {{ color: red; background: #ffe6e6; }}
|
|
.success {{ border-left: 4px solid #4CAF50; }}
|
|
.failure {{ border-left: 4px solid #f44336; }}
|
|
.stats {{ background: #e3f2fd; padding: 15px; margin-bottom: 20px; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>OMML to LaTeX Conversion Results</h1>
|
|
|
|
<div class="stats">
|
|
<h3>Conversion Statistics</h3>
|
|
<p>Total equations: {total}</p>
|
|
<p>Successful: {successful}</p>
|
|
<p>Failed: {failed}</p>
|
|
<p>Success rate: {rate:.1f}%</p>
|
|
</div>
|
|
|
|
{equations_html}
|
|
</body>
|
|
</html>"""
|
|
|
|
successful = sum(1 for r in results if r.success)
|
|
failed = len(results) - successful
|
|
success_rate = (successful / len(results)) * 100 if results else 0
|
|
|
|
equations_html = []
|
|
|
|
for i, result in enumerate(results):
|
|
equation_class = "success" if result.success else "failure"
|
|
|
|
if result.success:
|
|
latex_formatted = format_latex([result], format_type, delimiter).strip()
|
|
|
|
equation_html = f"""
|
|
<div class="equation {equation_class}">
|
|
<h4>Equation {i+1}</h4>
|
|
{f'<div class="context">Context: {result.context}</div>' if result.context else ''}
|
|
<div class="latex-code">LaTeX: {result.latex}</div>
|
|
<div>Rendered: {latex_formatted}</div>
|
|
</div>
|
|
"""
|
|
else:
|
|
equation_html = f"""
|
|
<div class="equation {equation_class}">
|
|
<h4>Equation {i+1} - ERROR</h4>
|
|
{f'<div class="context">Context: {result.context}</div>' if result.context else ''}
|
|
<div class="error">Error: {result.error}</div>
|
|
</div>
|
|
"""
|
|
|
|
equations_html.append(equation_html)
|
|
|
|
return html_template.format(
|
|
total=len(results),
|
|
successful=successful,
|
|
failed=failed,
|
|
rate=success_rate,
|
|
equations_html="\n".join(equations_html)
|
|
)
|