服务: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 默认密钥已清空
428 lines
20 KiB
Java
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("<", "<").replaceAll(">", ">");
|
|
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();
|
|
}
|
|
}
|
|
|