Files
DoLuEdu-WordFileParse/java-src/utils/finder/CtoMathFinder.java
Quentin 3271fee5cc 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 默认密钥已清空
2026-09-29 09:43:13 +08:00

428 lines
20 KiB
Java

/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* javax.xml.bind.JAXBElement
* org.apache.commons.collections4.CollectionUtils
* org.apache.commons.lang3.StringUtils
* org.docx4j.TraversalUtil$CallbackImpl
* org.docx4j.XmlUtils
* org.docx4j.com.microsoft.schemas.office.word.x2010.wordprocessingShape.CTTextboxInfo
* org.docx4j.math.CTOMath
* org.docx4j.math.CTOMathPara
* org.docx4j.wml.CTTxbxContent
* org.docx4j.wml.CTVerticalAlignRun
* org.docx4j.wml.FldChar
* org.docx4j.wml.ObjectFactory
* org.docx4j.wml.P
* org.docx4j.wml.PPr
* org.docx4j.wml.PPrBase$NumPr
* org.docx4j.wml.PPrBase$NumPr$Ilvl
* org.docx4j.wml.R
* org.docx4j.wml.RPrAbstract
* org.docx4j.wml.STFldCharType
* org.docx4j.wml.Style
* org.docx4j.wml.Text
* org.docx4j.wml.U
* org.docx4j.wml.UnderlineEnumeration
* org.slf4j.Logger
* org.slf4j.LoggerFactory
*/
package com.jby.officecore.utils.finder;
import com.jby.officecore.domain.bo.ParseBO;
import com.jby.officecore.utils.EqToLatexUtil;
import com.jby.officecore.utils.NumPr;
import com.jby.officecore.utils.WordParseUtil;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.bind.JAXBElement;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.docx4j.TraversalUtil;
import org.docx4j.XmlUtils;
import org.docx4j.com.microsoft.schemas.office.word.x2010.wordprocessingShape.CTTextboxInfo;
import org.docx4j.math.CTOMath;
import org.docx4j.math.CTOMathPara;
import org.docx4j.wml.CTTxbxContent;
import org.docx4j.wml.CTVerticalAlignRun;
import org.docx4j.wml.FldChar;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.P;
import org.docx4j.wml.PPr;
import org.docx4j.wml.PPrBase;
import org.docx4j.wml.R;
import org.docx4j.wml.RPrAbstract;
import org.docx4j.wml.STFldCharType;
import org.docx4j.wml.Style;
import org.docx4j.wml.Text;
import org.docx4j.wml.U;
import org.docx4j.wml.UnderlineEnumeration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CtoMathFinder
extends TraversalUtil.CallbackImpl {
private static final Logger log = LoggerFactory.getLogger(CtoMathFinder.class);
private ParseBO parseBO;
private static final Pattern CIRCLED_NUMBER_PATTERN = Pattern.compile("^\\s*eq\\s*\\\\o\\((?:\u25cb,\\\\s\\\\up\\d+\\((\\d+)\\)|\\\\s\\\\up\\d+\\((\\d+)\\),\u25cb)\\)");
public CtoMathFinder(ParseBO parseBO) {
this.parseBO = parseBO;
}
public List<Object> apply(Object o) {
if (o instanceof P) {
Text text;
Style style;
CTTxbxContent ctTxbxContent;
StringBuilder eqCode = new StringBuilder();
StringBuilder originEqCode = new StringBuilder();
boolean inFiled = false;
boolean isEq = false;
boolean superScriptFlag = false;
boolean subScriptFlag = false;
ArrayList<Integer> fieldRunIndexs = new ArrayList<Integer>();
P paragraph = (P)o;
ObjectFactory factory = new ObjectFactory();
boolean isNumPr = true;
if (paragraph.getParent() instanceof CTTxbxContent && (ctTxbxContent = (CTTxbxContent)paragraph.getParent()).getParent() instanceof CTTextboxInfo) {
isNumPr = false;
}
PPr pPr = paragraph.getPPr();
PPrBase.NumPr numPr = null;
if (Objects.nonNull(pPr)) {
numPr = pPr.getNumPr();
}
if (Objects.isNull(numPr) && Objects.nonNull(pPr) && Objects.nonNull(pPr.getPStyle()) && Objects.nonNull(style = this.parseBO.getStyleDefinitionsPart().getStyleById(pPr.getPStyle().getVal())) && Objects.nonNull(style.getPPr()) && Objects.nonNull(style.getPPr().getNumPr()) && Objects.isNull((numPr = style.getPPr().getNumPr()).getIlvl())) {
PPrBase.NumPr.Ilvl ilvl = new PPrBase.NumPr.Ilvl();
ilvl.setVal(BigInteger.ZERO);
numPr.setIlvl(ilvl);
}
if (isNumPr && Objects.nonNull(numPr) && Objects.nonNull(numPr.getNumId()) && Objects.nonNull(numPr.getIlvl())) {
String numPrKey = numPr.getNumId().getVal() + "_" + numPr.getIlvl().getVal();
Map<String, NumPr> numberingDefinitionsMap = this.parseBO.getNumberingDefinitionsMap();
if (numberingDefinitionsMap.containsKey(numPrKey)) {
NumPr currentNumPr = numberingDefinitionsMap.get(numPrKey);
String lvlText = currentNumPr.getLvlText();
for (int i = 0; i <= numPr.getIlvl().getVal().intValue(); ++i) {
NumPr numPrDefine;
if (i == numPr.getIlvl().getVal().intValue()) {
currentNumPr.setCurrent(currentNumPr.getCurrent().add(BigInteger.ONE));
}
String rep = String.valueOf((numPrDefine = numberingDefinitionsMap.get(numPr.getNumId().getVal() + "_" + i)).getCurrent().compareTo(BigInteger.ZERO) == 0 ? numPrDefine.getStart() : numPrDefine.getCurrent());
if (i == numPr.getIlvl().getVal().intValue()) {
switch (currentNumPr.getNumFmt()) {
case "UPPER_ROMAN": {
rep = this.strToUpperRoman(rep);
break;
}
case "DECIMAL_ZERO": {
rep = CtoMathFinder.strToDecimalZero(rep);
break;
}
case "LOWER_LETTER": {
rep = CtoMathFinder.strToLowerLetter(rep);
break;
}
case "UPPER_LETTER": {
rep = CtoMathFinder.strToLowerLetter(rep).toUpperCase();
break;
}
case "LOWER_ROMAN": {
rep = this.strToUpperRoman(rep).toLowerCase();
break;
}
case "CHINESE_COUNTING":
case "CHINESE_COUNTING_THOUSAND": {
rep = this.strToChineseCounting(rep);
break;
}
}
}
lvlText = lvlText.replace("%" + (i + 1), rep);
}
if (StringUtils.isNotBlank((CharSequence)lvlText)) {
R run = factory.createR();
text = factory.createText();
text.setValue(lvlText + " ");
run.getContent().add(text);
paragraph.getContent().add(0, run);
}
}
}
for (int i = 0; i < paragraph.getContent().size(); ++i) {
JAXBElement jaxbElement;
Object content = paragraph.getContent().get(i);
if (content instanceof JAXBElement && ((jaxbElement = (JAXBElement)content).getValue() instanceof CTOMath || jaxbElement.getValue() instanceof CTOMathPara)) {
String placeholder = "";
if (Boolean.TRUE.equals(this.parseBO.getNewParseVersion())) {
String omml = XmlUtils.marshaltoString((Object)jaxbElement);
placeholder = this.parseBO.getOpenOfficeXmlPlaceholder();
this.parseBO.getOpenOfficeXmlMap().put(placeholder, omml);
} else {
String mathMl = WordParseUtil.getMathMl(jaxbElement, this.parseBO.getTransformer());
placeholder = this.parseBO.getOpenOfficeXmlPlaceholder();
this.parseBO.getOpenOfficeXmlMap().put(placeholder, mathMl);
}
R run = factory.createR();
text = factory.createText();
text.setValue(placeholder);
run.getContent().add(text);
paragraph.getContent().set(i, run);
}
if (!(content instanceof R)) continue;
R run = (R)content;
for (int j = 0; j < run.getContent().size(); ++j) {
Object runC = run.getContent().get(j);
if (!(runC instanceof JAXBElement)) continue;
JAXBElement runCj = (JAXBElement)runC;
if (runCj.getValue() instanceof FldChar) {
FldChar fldChar = (FldChar)runCj.getValue();
if (fldChar.getFldCharType().equals((Object)STFldCharType.BEGIN)) {
eqCode = new StringBuilder();
originEqCode = new StringBuilder();
inFiled = true;
isEq = false;
superScriptFlag = false;
subScriptFlag = false;
fieldRunIndexs = new ArrayList();
fieldRunIndexs.add(i);
continue;
}
if (inFiled && fldChar.getFldCharType().equals((Object)STFldCharType.END)) {
if (isEq) {
String placeholder;
Matcher matcher = CIRCLED_NUMBER_PATTERN.matcher(originEqCode);
if (matcher.matches()) {
String numStr = matcher.group(1) != null ? matcher.group(1) : matcher.group(2);
placeholder = this.parseBO.getEqLatexPlaceholder() + "JBYEQNUM";
this.parseBO.getEqLatexMap().put(placeholder, CtoMathFinder.getCircledNumber(numStr));
R runN = factory.createR();
Text text2 = factory.createText();
text2.setValue(placeholder);
runN.getContent().add(text2);
paragraph.getContent().set(i, runN);
} else if (StringUtils.isNotBlank((CharSequence)eqCode.toString())) {
String latex = EqToLatexUtil.convert(eqCode.toString());
latex = latex.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
placeholder = this.parseBO.getEqLatexPlaceholder();
this.parseBO.getEqLatexMap().put(placeholder, latex);
R runN = factory.createR();
Text text3 = factory.createText();
text3.setValue(placeholder);
runN.getContent().add(text3);
paragraph.getContent().set(i, runN);
}
}
inFiled = false;
isEq = false;
fieldRunIndexs.clear();
continue;
}
if (inFiled && isEq && !fldChar.getFldCharType().value().equals("separate")) continue;
}
if (!inFiled || !(runCj.getValue() instanceof Text)) continue;
CTVerticalAlignRun vertAlign = Optional.ofNullable(run.getRPr()).map(RPrAbstract::getVertAlign).orElse(null);
String value = ((Text)runCj.getValue()).getValue();
originEqCode.append(value);
U u = Optional.ofNullable(run.getRPr()).filter(f -> Objects.nonNull(f.getU()) && !UnderlineEnumeration.NONE.equals((Object)f.getU().getVal()) && Objects.nonNull(f.getU().getVal())).map(RPrAbstract::getU).orElse(null);
if (StringUtils.isBlank((CharSequence)value) && Objects.nonNull(u)) {
value = value.replace("\u3000", "JBYU1JBYU").replace(" ", "JBYU1JBYU");
} else if (StringUtils.isNotBlank((CharSequence)value) && Objects.nonNull(u)) {
value = "JBYU2BEGIN" + value + "JBYU2END";
} else if (StringUtils.isBlank((CharSequence)value)) {
// empty if block
}
String trimmedOrigin = CtoMathFinder.extractEqPrefix(originEqCode);
if (trimmedOrigin.startsWith("eq")) {
isEq = true;
}
if (isEq) {
if (CollectionUtils.isNotEmpty(fieldRunIndexs)) {
fieldRunIndexs.forEach(m -> paragraph.getContent().set((int)m, factory.createR()));
fieldRunIndexs.clear();
}
if (Objects.nonNull(vertAlign)) {
String newContent;
String tmp;
if ("superscript".equalsIgnoreCase(vertAlign.getVal().toString())) {
int end;
int start;
if (superScriptFlag && (start = eqCode.lastIndexOf("\\s\\up4(")) != -1 && (end = eqCode.toString().indexOf(41, start)) != -1) {
tmp = eqCode.substring(start + 7, end);
newContent = tmp + value;
eqCode = new StringBuilder(eqCode.substring(0, start));
eqCode.append("\\s\\up4(").append(newContent).append(")");
value = "";
}
if (StringUtils.isNotBlank((CharSequence)value)) {
value = "\\s\\up4(" + value + ")";
}
superScriptFlag = true;
subScriptFlag = false;
} else if ("subscript".equalsIgnoreCase(vertAlign.getVal().toString())) {
int end;
int start;
if (subScriptFlag && (start = eqCode.lastIndexOf("\\s\\do4(")) != -1 && (end = eqCode.toString().indexOf(41, start)) != -1) {
tmp = eqCode.substring(start + 7, end);
newContent = tmp + value;
eqCode = new StringBuilder(eqCode.substring(0, start));
eqCode.append("\\s\\do4(").append(newContent).append(")");
value = "";
}
if (StringUtils.isNotBlank((CharSequence)value)) {
value = "\\s\\do4(" + value + ")";
}
subScriptFlag = true;
superScriptFlag = false;
} else {
superScriptFlag = false;
subScriptFlag = false;
}
} else {
superScriptFlag = false;
subScriptFlag = false;
}
eqCode.append(value);
paragraph.getContent().set(i, factory.createR());
continue;
}
if (trimmedOrigin.length() < 2 || "eq".startsWith(trimmedOrigin)) {
eqCode.append(value);
fieldRunIndexs.add(i);
continue;
}
inFiled = false;
fieldRunIndexs.clear();
}
}
}
return null;
}
private String strToUpperRoman(String str) {
int num = Integer.parseInt(str);
if (num < 1 || num > 3999) {
return "I";
}
int[] values = new int[]{1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
String[] symbols = new String[]{"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
StringBuilder roman = new StringBuilder();
for (int i = 0; i < values.length; ++i) {
while (num >= values[i]) {
roman.append(symbols[i]);
num -= values[i];
}
}
return roman.toString();
}
private static String strToDecimalZero(String str) {
if (str.length() == 1) {
return "0" + str;
}
return str;
}
private static String strToLowerLetter(String str) {
int n = Integer.parseInt(str);
int length = (n - 1) / 26 + 1;
char baseChar = (char)(97 + (n - 1) % 26);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; ++i) {
sb.append(baseChar);
}
return sb.toString();
}
public String strToChineseCounting(String str) {
String s;
int num = Integer.parseInt(str);
if (num < 0 || num >= 1000000000) {
throw new IllegalArgumentException("\u4ec5\u652f\u6301 0 \u5230 999,999,999");
}
if (num == 0) {
return "\u3007";
}
char[] digits = new char[]{'\u3007', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d', '\u4e03', '\u516b', '\u4e5d'};
String[] units = new String[]{"", "\u5341", "\u767e", "\u5343"};
String[] bigUnits = new String[]{"", "\u4e07", "\u4ebf"};
StringBuilder result = new StringBuilder();
int yi = num / 100000000;
int wan = num / 10000 % 10000;
int ge = num % 10000;
if (yi > 0) {
result.append(CtoMathFinder.convertSegment(yi, digits, units)).append(bigUnits[2]);
if (wan == 0 && ge > 0) {
result.append('\u3007');
}
}
if (wan > 0) {
result.append(CtoMathFinder.convertSegment(wan, digits, units)).append(bigUnits[1]);
if (ge > 0 && ge < 1000) {
result.append('\u3007');
}
}
if (ge > 0) {
result.append(CtoMathFinder.convertSegment(ge, digits, units));
}
if ((s = result.toString()).startsWith("\u4e00\u5341")) {
s = s.substring(1);
}
return s;
}
private static String convertSegment(int n, char[] digits, String[] units) {
if (n == 0) {
return "";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 4; ++i) {
int power = 3 - i;
int base = (int)Math.pow(10.0, power);
int digit = n / base % 10;
if (digit != 0) {
sb.append(digits[digit]).append(units[power]);
continue;
}
if (sb.length() <= 0 || n % base == 0 || power <= 0) continue;
sb.append('\u3007');
}
return sb.toString();
}
private static String getCircledNumber(String num) {
int n = Integer.parseInt(num);
if (n >= 1 && n <= 20) {
return String.valueOf((char)(9312 + n - 1));
}
if (n >= 21 && n <= 35) {
return String.valueOf((char)(12881 + n - 21));
}
if (n >= 36 && n <= 40) {
return String.valueOf((char)(12977 + n - 36));
}
if (n >= 41 && n <= 50) {
return String.valueOf((char)(12982 + n - 41));
}
log.info("\u83b7\u53d6\u5e26\u5708\u5b57\u7b26\u5931\u8d25 {}", (Object)num);
return "";
}
private static String extractEqPrefix(StringBuilder originEqCode) {
int i;
String s = originEqCode.toString().trim();
for (i = 0; i < s.length() && s.charAt(i) == '{'; ++i) {
}
return s.substring(i).trim().toLowerCase();
}
}