init: WordFileParse docx→HTML 解析服务(含 Word EQ 域公式修复)
服务: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 默认密钥已清空
This commit is contained in:
221
scripts/omml_mathml2latex/processing/output_formatter.py
Normal file
221
scripts/omml_mathml2latex/processing/output_formatter.py
Normal file
@@ -0,0 +1,221 @@
|
||||
"""
|
||||
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)
|
||||
)
|
||||
Reference in New Issue
Block a user