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:
2026-09-29 09:43:13 +08:00
commit 3271fee5cc
360 changed files with 86082 additions and 0 deletions

36
java-svc/Dockerfile Normal file
View File

@@ -0,0 +1,36 @@
# WordFileParse Java Service — Docker build with pre-built JAR
# First build JAR locally: mvn package -DskipTests
# Then: docker build -t wordparse-java -f java-svc/Dockerfile .
FROM eclipse-temurin:17-jdk
# Install Ruby + Python + build deps
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip python3-venv \
ruby ruby-dev build-essential libxml2-dev libxslt-dev pkg-config \
&& rm -rf /var/lib/apt/lists/*
# Install pre-built Ruby gems (except nokogiri — compiled for current Ruby)
COPY scripts/mathtype2mathml/install/gems/one/*.gem \
scripts/mathtype2mathml/install/gems/two/bindata-*.gem \
scripts/mathtype2mathml/install/gems/two/concurrent-ruby-*.gem \
scripts/mathtype2mathml/install/gems/two/mini_portile2-*.gem \
scripts/mathtype2mathml/install/gems/two/pry-*.gem \
scripts/mathtype2mathml/install/gems/two/ruby-ole-*.gem \
scripts/mathtype2mathml/install/gems/third/parallel-*.gem \
/tmp/gems/
RUN gem install /tmp/gems/*.gem --no-doc && rm -rf /tmp/gems
RUN gem install nokogiri --no-doc
WORKDIR /app
# Copy transform scripts
COPY scripts/ /opt/transform/
# Python deps for omml_mathml2latex
RUN pip3 install --break-system-packages -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple lxml
# Copy pre-built JAR
COPY java-svc/target/word-file-parse-1.0.0.jar /app/app.jar
EXPOSE 8080
CMD ["java", "-jar", "/app/app.jar"]

136
java-svc/pom.xml Normal file
View File

@@ -0,0 +1,136 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</parent>
<groupId>com.wordparse</groupId>
<artifactId>word-file-parse</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>WordFileParse</name>
<properties>
<java.version>17</java.version>
<docx4j.version>8.3.9</docx4j.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Docx4J 8.x (Word → HTML) -->
<dependency>
<groupId>org.docx4j</groupId>
<artifactId>docx4j-core</artifactId>
<version>${docx4j.version}</version>
</dependency>
<dependency>
<groupId>org.docx4j</groupId>
<artifactId>docx4j-JAXB-ReferenceImpl</artifactId>
<version>${docx4j.version}</version>
</dependency>
<!-- Jsoup (HTML post-processing) -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.17.2</version>
</dependency>
<!-- Apache Commons -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.16.1</version>
</dependency>
<!-- FastJSON -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.51</version>
</dependency>
<!-- AWS S3 SDK (RustFS) -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.25.27</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>net.sf.saxon</groupId>
<artifactId>Saxon-HE</artifactId>
<version>12.5</version>
</dependency>
<!-- ImageIO for TIFF handling -->
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-tiff</artifactId>
<version>3.11.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,13 @@
package com.wordparse;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableConfigurationProperties
public class WordParseApplication {
public static void main(String[] args) {
SpringApplication.run(WordParseApplication.class, args);
}
}

View File

@@ -0,0 +1,28 @@
package com.wordparse.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
@Configuration
@ConfigurationProperties(prefix = "wordparse")
public class WordParseConfig {
private String tmpPath = System.getProperty("java.io.tmpdir");
private Double imageScalingFactor = 2.0;
private Map<String, String[]> cleanStyleMap = Map.of();
private String transformScriptsPath = "/opt/transform";
private String pythonCommand = "python3";
public String getTmpPath() { return tmpPath; }
public Double getImageScalingFactor() { return imageScalingFactor; }
public Map<String, String[]> getCleanStyleMap() { return cleanStyleMap; }
public String getTransformScriptsPath() { return transformScriptsPath; }
public String getPythonCommand() { return pythonCommand; }
public void setTmpPath(String v) { this.tmpPath = v; }
public void setImageScalingFactor(Double v) { this.imageScalingFactor = v; }
public void setCleanStyleMap(Map<String, String[]> v) { this.cleanStyleMap = v; }
public void setTransformScriptsPath(String v) { this.transformScriptsPath = v; }
public void setPythonCommand(String v) { this.pythonCommand = v; }
}

View File

@@ -0,0 +1,53 @@
package com.wordparse.controller;
import com.wordparse.core.WordParseUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/v3/server_office_file/file")
public class WordParseController {
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(WordParseController.class);
private final WordParseUtil wordParseUtil;
public WordParseController(WordParseUtil wordParseUtil) {
this.wordParseUtil = wordParseUtil;
}
@PostMapping(value = "/parse", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Map<String, Object> parseFile(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "fileId", required = false) String fileId) {
if (file.isEmpty()) throw new IllegalArgumentException("File is empty");
String name = file.getOriginalFilename();
if (name == null || !name.toLowerCase().endsWith(".docx")) {
throw new IllegalArgumentException("Only .docx files supported");
}
try {
String html = wordParseUtil.parseBytes(file.getBytes());
String fid = fileId != null ? fileId : UUID.randomUUID().toString();
log.info("Parse success: file={}, fileId={}, htmlLen={}", name, fid, html.length());
return Map.of("fileId", fid, "content", html);
} catch (Exception e) {
log.error("Parse failed: {}", name, e);
throw new RuntimeException("Parse failed: " + e.getMessage(), e);
}
}
@GetMapping("/health")
public Map<String, String> health() {
return Map.of("status", "ok");
}
}

View File

@@ -0,0 +1,46 @@
/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* org.docx4j.TraversalUtil$CallbackImpl
* org.docx4j.openpackaging.packages.WordprocessingMLPackage
* org.docx4j.openpackaging.parts.Part
* org.docx4j.openpackaging.parts.relationships.RelationshipsPart
* org.docx4j.vml.officedrawing.CTOLEObject
*/
package com.wordparse.core;
import com.wordparse.core.ParseBO;
import java.util.List;
import java.util.Objects;
import org.docx4j.TraversalUtil;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.Part;
import org.docx4j.openpackaging.parts.relationships.RelationshipsPart;
import org.docx4j.vml.officedrawing.CTOLEObject;
public class CtObjectFinder
extends TraversalUtil.CallbackImpl {
private final WordprocessingMLPackage wordMlPackage;
private final ParseBO parseBO;
public CtObjectFinder(WordprocessingMLPackage wordMlPackage, ParseBO parseBO) {
this.wordMlPackage = wordMlPackage;
this.parseBO = parseBO;
}
public List<Object> apply(Object o) {
if (o instanceof CTOLEObject) {
CTOLEObject ctoleObject = (CTOLEObject)o;
RelationshipsPart rsPart = this.wordMlPackage.getMainDocumentPart().getRelationshipsPart();
if (Objects.nonNull(ctoleObject.getId())) {
Part part = rsPart.getPart(ctoleObject.getId());
String binFilePath = part.getPartName().getURI().getPath();
String placeholder = this.parseBO.getMathTypePlaceholder(ctoleObject.getId());
this.parseBO.getMathTypeMap().put(placeholder, this.parseBO.getTempDir() + "/temp" + binFilePath);
}
}
return null;
}
}

View File

@@ -0,0 +1,123 @@
/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* javax.xml.bind.JAXBElement
* org.apache.commons.lang3.StringUtils
* org.docx4j.TextUtils
* org.docx4j.TraversalUtil$CallbackImpl
* org.docx4j.mce.AlternateContent
* org.docx4j.mce.AlternateContent$Fallback
* org.docx4j.openpackaging.packages.WordprocessingMLPackage
* org.docx4j.vml.CTGroup
* org.docx4j.vml.CTRoundRect
* org.docx4j.vml.CTShape
* org.docx4j.vml.CTTextbox
* org.docx4j.vml.VmlShapeElements
* org.docx4j.wml.CTTxbxContent
* org.docx4j.wml.ObjectFactory
* org.docx4j.wml.Pict
* org.docx4j.wml.R
* org.docx4j.wml.Text
* org.slf4j.Logger
* org.slf4j.LoggerFactory
*/
package com.wordparse.core;
import com.wordparse.core.ParseBO;
import java.util.List;
import javax.xml.bind.JAXBElement;
import org.apache.commons.lang3.StringUtils;
import org.docx4j.TextUtils;
import org.docx4j.TraversalUtil;
import org.docx4j.mce.AlternateContent;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.vml.CTGroup;
import org.docx4j.vml.CTRoundRect;
import org.docx4j.vml.CTShape;
import org.docx4j.vml.CTTextbox;
import org.docx4j.vml.VmlShapeElements;
import org.docx4j.wml.CTTxbxContent;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.Pict;
import org.docx4j.wml.R;
import org.docx4j.wml.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@SuppressWarnings({"rawtypes", "unchecked"})
public class CtTextBoxFinder extends TraversalUtil.CallbackImpl {
private static final Logger log = LoggerFactory.getLogger(CtTextBoxFinder.class);
private WordprocessingMLPackage wordMlPackage;
private ParseBO parseBO;
public CtTextBoxFinder(WordprocessingMLPackage wordMlPackage, ParseBO parseBO) {
this.wordMlPackage = wordMlPackage;
this.parseBO = parseBO;
}
public List<Object> apply(Object o) {
if (o instanceof Pict) {
boolean isFirstRun = true;
ObjectFactory factory = new ObjectFactory();
StringBuilder stringBuilder = new StringBuilder();
R sourceR = factory.createR();
Pict pict = (Pict)o;
List anyAndAny = pict.getAnyAndAny();
for (Object object : anyAndAny) {
if (!(object instanceof JAXBElement)) continue;
JAXBElement jbe = (JAXBElement)object;
this.findTextBox(jbe.getValue(), stringBuilder, isFirstRun, sourceR);
}
if (StringUtils.isNotBlank((CharSequence)stringBuilder.toString())) {
AlternateContent.Fallback fallback = (AlternateContent.Fallback)pict.getParent();
AlternateContent alternateContent = (AlternateContent)fallback.getParent();
R r = (R)alternateContent.getParent();
Text text = factory.createText();
text.setValue(stringBuilder + "WP_TEXTBOX_WP");
r.getContent().set(0, text);
r.setRPr(sourceR.getRPr());
}
}
return null;
}
private void findTextBox(Object object, StringBuilder sb, boolean isFirstRun, R sourceR) {
block8: {
List egShapeElements;
if (object instanceof CTShape || object instanceof CTRoundRect) {
VmlShapeElements vmlShapeElements = (VmlShapeElements)object;
egShapeElements = vmlShapeElements.getEGShapeElements();
for (Object o : egShapeElements) {
JAXBElement egShapeElement = (JAXBElement) o;
if (!(egShapeElement.getValue() instanceof CTTextbox)) continue;
CTTextbox ctTextbox = (CTTextbox)egShapeElement.getValue();
CTTxbxContent txbxContent = ctTextbox.getTxbxContent();
for (int i = 0; i < txbxContent.getContent().size(); ++i) {
Object obj = txbxContent.getContent().get(i);
String text = TextUtils.getText(obj);
if (txbxContent.getContent().size() > 1 && i != txbxContent.getContent().size() - 1) {
text = text + "WP_TEXTBOX_BR";
}
sb.append(text);
}
}
}
if (!(object instanceof CTGroup)) break block8;
CTGroup ctGroup = (CTGroup)object;
egShapeElements = ctGroup.getEGShapeElements();
boolean isCtShape = egShapeElements.stream().anyMatch(f -> ((JAXBElement)f).getValue() instanceof CTShape);
if (isCtShape) {
for (int i = egShapeElements.size() - 1; i >= 0; --i) {
this.findTextBox(((JAXBElement)egShapeElements.get(i)).getValue(), sb, isFirstRun, sourceR);
}
} else {
for (Object o : egShapeElements) {
JAXBElement egShapeElement = (JAXBElement) o;
this.findTextBox(egShapeElement.getValue(), sb, isFirstRun, sourceR);
}
}
}
}
}

View File

@@ -0,0 +1,421 @@
/*
* 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.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.wordparse.core;
import com.wordparse.core.ParseBO;
import com.wordparse.core.NumPr;
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.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) {
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()) && this.parseBO.getStyleDefinitionsPart() != null) {
// Style-based numPr lookup — skipped (StyleDefinitionsPart not ported)
}
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() + "WPEQNUM";
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() + "WPEQNUM";
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", "WPU1WPU").replace(" ", "WPU1WPU");
} else if (StringUtils.isNotBlank((CharSequence)value) && Objects.nonNull(u)) {
value = "WPU2BEGIN" + value + "WPU2END";
} 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();
}
}

View File

@@ -0,0 +1,433 @@
/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* com.alibaba.excel.util.StringUtils
* java.util.UUID
* javax.xml.bind.JAXBElement
* org.apache.commons.collections4.CollectionUtils
* org.docx4j.TraversalUtil$CallbackImpl
* org.docx4j.dml.GraphicData
* org.docx4j.dml.picture.Pic
* org.docx4j.dml.wordprocessingDrawing.Anchor
* org.docx4j.dml.wordprocessingDrawing.Inline
* org.docx4j.openpackaging.packages.WordprocessingMLPackage
* org.docx4j.openpackaging.parts.Part
* org.docx4j.openpackaging.parts.WordprocessingML.BinaryPartAbstractImage
* org.docx4j.openpackaging.parts.relationships.RelationshipsPart
* org.docx4j.vml.CTImageData
* org.docx4j.vml.CTShape
* org.docx4j.vml.officedrawing.CTOLEObject
* org.docx4j.wml.CTObject
* org.docx4j.wml.Drawing
* org.docx4j.wml.ObjectFactory
* org.docx4j.wml.Pict
* org.docx4j.wml.R
* org.docx4j.wml.Text
* org.slf4j.Logger
* org.slf4j.LoggerFactory
*/
package com.wordparse.core;
import java.util.UUID;
import com.wordparse.core.ImageBO;
import com.wordparse.core.ParseBO;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
import org.apache.commons.lang3.StringUtils;
import javax.xml.bind.JAXBElement;
import org.apache.commons.collections4.CollectionUtils;
import org.docx4j.TraversalUtil;
import org.docx4j.dml.GraphicData;
import org.docx4j.dml.picture.Pic;
import org.docx4j.dml.wordprocessingDrawing.Anchor;
import org.docx4j.dml.wordprocessingDrawing.Inline;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.Part;
import org.docx4j.openpackaging.parts.WordprocessingML.BinaryPartAbstractImage;
import org.docx4j.openpackaging.parts.relationships.RelationshipsPart;
import org.docx4j.vml.CTImageData;
import org.docx4j.vml.CTShape;
import org.docx4j.vml.officedrawing.CTOLEObject;
import org.docx4j.wml.CTObject;
import org.docx4j.wml.Drawing;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.Pict;
import org.docx4j.wml.R;
import org.docx4j.wml.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DrawingPictFinder
extends TraversalUtil.CallbackImpl {
private static final Logger log = LoggerFactory.getLogger(DrawingPictFinder.class);
private final WordprocessingMLPackage wordMlPackage;
private final ParseBO parseBO;
private static final double CROP_SCALE = 1.52587890625;
public DrawingPictFinder(WordprocessingMLPackage wordMlPackage, ParseBO parseBO) {
this.wordMlPackage = wordMlPackage;
this.parseBO = parseBO;
}
public List<Object> apply(Object o) {
List contentList;
List anyAndAny;
R r;
if (o instanceof Drawing && ((Drawing)o).getParent() instanceof R) {
RelationshipsPart relationshipsPart = this.wordMlPackage.getMainDocumentPart().getRelationshipsPart();
Drawing drawing = (Drawing)o;
List anchorOrInline = drawing.getAnchorOrInline();
if (CollectionUtils.isNotEmpty((Collection)anchorOrInline)) {
Object anchorOrInlineObj = anchorOrInline.get(0);
GraphicData graphicData = null;
long cx = 0L;
long cy = 0L;
if (anchorOrInlineObj instanceof Inline) {
Inline inline = (Inline)anchorOrInlineObj;
graphicData = inline.getGraphic().getGraphicData();
cx = inline.getExtent().getCx();
cy = inline.getExtent().getCy();
} else if (anchorOrInlineObj instanceof Anchor) {
Anchor anchor = (Anchor)anchorOrInlineObj;
graphicData = anchor.getGraphic().getGraphicData();
cx = anchor.getExtent().getCx();
cy = anchor.getExtent().getCy();
}
if (Objects.nonNull(graphicData) && "http://schemas.openxmlformats.org/drawingml/2006/picture".equals(graphicData.getUri())) {
for (Object object : graphicData.getAny()) {
String path;
JAXBElement jaxbElement;
if (!(object instanceof JAXBElement) || !((jaxbElement = (JAXBElement)object).getValue() instanceof Pic)) continue;
Pic pic = (Pic)jaxbElement.getValue();
String imageDataId = null;
int cropLeft = 0;
int cropTop = 0;
int cropRight = 0;
int cropBottom = 0;
int rotation = 0;
if (Objects.nonNull(pic.getBlipFill())) {
if (Objects.nonNull(pic.getBlipFill().getBlip())) {
imageDataId = pic.getBlipFill().getBlip().getEmbed();
}
if (Objects.nonNull(pic.getBlipFill().getSrcRect())) {
cropLeft = pic.getBlipFill().getSrcRect().getL();
cropTop = pic.getBlipFill().getSrcRect().getT();
cropRight = pic.getBlipFill().getSrcRect().getR();
cropBottom = pic.getBlipFill().getSrcRect().getB();
}
if (Objects.nonNull(pic.getSpPr()) && Objects.nonNull(pic.getSpPr().getXfrm())) {
rotation = pic.getSpPr().getXfrm().getRot();
}
}
Map<String, Object> map = this.dealCropRotate(imageDataId, cx, cropLeft, cropTop, cropRight, cropBottom, rotation);
Object widthObj = map.get("width");
Object heightObj = map.get("height");
Object pathObj = map.get("imagePath");
double width = Objects.nonNull(widthObj) ? Double.parseDouble(widthObj.toString()) : (double)Math.round((double)cx / 9525.0);
double height = Objects.nonNull(heightObj) ? Double.parseDouble(heightObj.toString()) : (double)Math.round((double)cy / 9525.0);
Part part = relationshipsPart.getPart(imageDataId);
if (Objects.nonNull(pathObj)) {
path = String.valueOf(pathObj);
} else if (Objects.nonNull(part)) {
path = part.getPartName().getURI().getPath();
} else {
log.warn("\u65e0\u6cd5\u83b7\u53d6\u56fe\u7247Part\uff0cimageDataId={}", (Object)imageDataId);
continue;
}
String imagePlaceholder = this.parseBO.getImagePlaceholder(imageDataId);
ImageBO imageBO = new ImageBO().setWidth(width).setHeight(height).setPictureUrl(path);
this.parseBO.getImageMap().put(imagePlaceholder, imageBO);
R r2 = (R)drawing.getParent();
List contentList2 = r2.getContent();
int drawingIndex = contentList2.indexOf(drawing);
if (drawingIndex != -1) {
Text text = new ObjectFactory().createText();
text.setValue(imagePlaceholder);
contentList2.set(drawingIndex, text);
continue;
}
log.warn("\u672a\u627e\u5230drawing\u5bf9\u8c61\u5728\u7236\u5143\u7d20\u4e2d\u7684\u7d22\u5f15\uff0c\u8df3\u8fc7\u66ff\u6362\u64cd\u4f5c");
}
}
}
}
if (o instanceof Pict && ((Pict)o).getParent() instanceof R) {
Pict pict = (Pict)o;
r = (R)pict.getParent();
anyAndAny = pict.getAnyAndAny();
contentList = r.getContent();
int pictIndex = contentList.indexOf(pict);
if (pictIndex != -1) {
this.dealCtShape(anyAndAny, r, pictIndex);
}
}
if (o instanceof CTObject && ((CTObject)o).getParent() instanceof R) {
CTObject ctObject = (CTObject)o;
r = (R)ctObject.getParent();
anyAndAny = ctObject.getAnyAndAny();
contentList = r.getContent();
int ctObjectIndex = contentList.indexOf(ctObject);
if (ctObjectIndex != -1) {
this.dealCtShape(anyAndAny, r, ctObjectIndex);
}
}
return null;
}
private String extractValue(String style, String property) {
if (style == null || property == null) {
return null;
}
String regex = "\\b" + Pattern.quote(property) + "\\s*:\\s*([\\d.]+[a-zA-Z]*)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(style);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
private void dealCtShape(List<Object> anyAndAny, R r, int pictIndex) {
String ctOleObjectRid = anyAndAny.stream().filter(f -> f instanceof JAXBElement && ((JAXBElement)f).getValue() instanceof CTOLEObject).map(f -> ((CTOLEObject)((JAXBElement)f).getValue()).getId()).filter(Objects::nonNull).findFirst().orElse(null);
if (Objects.nonNull(ctOleObjectRid) && Objects.nonNull(this.parseBO.getMathTypeMap().get(this.parseBO.getMathTypePlaceholder(ctOleObjectRid)))) {
Text text = new ObjectFactory().createText();
text.setValue(this.parseBO.getMathTypePlaceholder(ctOleObjectRid));
r.getContent().set(pictIndex, text);
} else if (Objects.isNull(ctOleObjectRid) || Objects.isNull(this.parseBO.getMathTypeMap().get(this.parseBO.getMathTypePlaceholder(ctOleObjectRid)))) {
if (Objects.nonNull(ctOleObjectRid)) {
this.parseBO.getMathTypeMap().remove(this.parseBO.getMathTypePlaceholder(ctOleObjectRid));
}
for (Object object : anyAndAny) {
String path;
double heightPx;
double widthPx;
if (!(object instanceof JAXBElement) || !(((JAXBElement)object).getValue() instanceof CTShape)) continue;
CTShape ctShape = (CTShape)((JAXBElement)object).getValue();
String width = this.extractValue(ctShape.getStyle(), "width");
String height = this.extractValue(ctShape.getStyle(), "height");
CTImageData ctImageData = ctShape.getEGShapeElements().stream().filter(f -> f.getValue() instanceof CTImageData).map(m -> (CTImageData)m.getValue()).findFirst().orElse(null);
if (!Objects.nonNull(ctImageData) || !StringUtils.isNotBlank((CharSequence)width) || !StringUtils.isNotBlank((CharSequence)height)) continue;
String ctImageDataId = ctImageData.getId();
long cx = 0L;
try {
double widthPtIn = Double.parseDouble(width.substring(0, width.length() - 2));
if (width.endsWith("pt")) {
cx = Math.round(widthPtIn * 12700.0);
} else if (width.endsWith("in")) {
cx = Math.round(widthPtIn * 914400.0);
}
}
catch (NumberFormatException ignored) {
log.error("extractWidth error");
}
int cropLeft = this.parseVmlCrop(ctImageData.getCropleft());
int cropTop = this.parseVmlCrop(ctImageData.getCroptop());
int cropRight = this.parseVmlCrop(ctImageData.getCropright());
int cropBottom = this.parseVmlCrop(ctImageData.getCropbottom());
String rotRaw = this.extractValue(ctShape.getStyle(), "rotation");
int rotation = rotRaw == null ? 0 : (int)Math.round((double)Long.parseLong(rotRaw.replaceAll("[^\\d-]", "")) * 0.91552734375);
Map<String, Object> map = this.dealCropRotate(ctImageDataId, cx, cropLeft, cropTop, cropRight, cropBottom, rotation);
Object widthObj = map.get("width");
Object heightObj = map.get("height");
Object pathObj = map.get("imagePath");
if (Objects.nonNull(widthObj) && Objects.nonNull(heightObj)) {
widthPx = (Double)map.get("width");
heightPx = (Double)map.get("height");
} else {
widthPx = Double.parseDouble(width.replaceAll("pt|in", ""));
if (width.endsWith("pt")) {
widthPx = Math.round(widthPx * 96.0 / 72.0);
} else if (width.endsWith("in")) {
widthPx = Math.round(widthPx * 96.0);
}
heightPx = Double.parseDouble(height.replaceAll("pt|in", ""));
if (height.endsWith("pt")) {
heightPx = heightPx * 96.0 / 72.0;
} else if (height.endsWith("in")) {
heightPx *= 96.0;
}
}
RelationshipsPart relationshipsPart = this.wordMlPackage.getMainDocumentPart().getRelationshipsPart();
Part part = relationshipsPart.getPart(ctImageDataId);
if (Objects.nonNull(pathObj)) {
path = String.valueOf(pathObj);
} else if (Objects.nonNull(part)) {
path = part.getPartName().getURI().getPath();
} else {
log.warn("\u65e0\u6cd5\u83b7\u53d6\u8001\u7248VML\u56fe\u7247Part\uff0cctImageDataId={}", (Object)ctImageDataId);
continue;
}
ImageBO imageBO = new ImageBO().setWidth(widthPx).setHeight(heightPx).setPictureUrl(path);
String imagePlaceholder = this.parseBO.getImagePlaceholder(ctImageDataId);
this.parseBO.getImageMap().put(imagePlaceholder, imageBO);
Text text = new ObjectFactory().createText();
text.setValue(imagePlaceholder);
r.getContent().set(pictIndex, text);
break;
}
}
}
private Map<String, Object> dealCropRotate(String imageDataId, long cx, int cropLeft, int cropTop, int cropRight, int cropBottom, int rotation) {
HashMap<String, Object> result = new HashMap<String, Object>();
try {
if (Objects.nonNull(imageDataId)) {
boolean isRotate;
boolean isCrop = cropLeft > 0 || cropTop > 0 || cropRight > 0 || cropBottom > 0;
boolean bl = isRotate = rotation != 0;
if (isCrop || isRotate) {
RelationshipsPart relationshipsPart = this.wordMlPackage.getMainDocumentPart().getRelationshipsPart();
Part part = relationshipsPart.getPart(imageDataId);
BinaryPartAbstractImage imagePart = (BinaryPartAbstractImage)part;
BufferedImage targetImage = ImageIO.read(new ByteArrayInputStream(imagePart.getBytes()));
if (isCrop) {
targetImage = this.cropImage(targetImage, cropLeft, cropTop, cropRight, cropBottom);
}
if (isRotate) {
targetImage = this.rotateImage(targetImage, rotation, cx, result);
}
if (Objects.nonNull(targetImage)) {
String formatName = this.getFormatName(imagePart);
String path = "/word/media/" + UUID.randomUUID().toString() + "." + formatName;
ImageIO.write((RenderedImage)targetImage, formatName, new File(this.parseBO.getTempDir() + "/temp" + path));
result.put("imagePath", path);
}
}
}
}
catch (Exception e) {
log.error("\u5904\u7406\u56fe\u7247\u88c1\u526a\u4e0e\u65cb\u8f6c\u5f02\u5e38", (Throwable)e);
}
return result;
}
private String getFormatName(BinaryPartAbstractImage imagePart) {
String contentType = imagePart.getContentType();
String formatName = contentType.contains("png") ? "png" : (contentType.contains("jpeg") ? "jpeg" : (contentType.contains("gif") ? "gif" : (contentType.contains("bmp") ? "bmp" : "png")));
return formatName;
}
public BufferedImage cropImage(BufferedImage originalImage, int srcRectL, int srcRectT, int srcRectR, int srcRectB) {
double leftRatio = srcRectL < 0 ? 0.0 : (double)srcRectL / 100000.0;
double topRatio = srcRectT < 0 ? 0.0 : (double)srcRectT / 100000.0;
double rightRatio = srcRectR < 0 ? 0.0 : (double)srcRectR / 100000.0;
double bottomRatio = srcRectB < 0 ? 0.0 : (double)srcRectB / 100000.0;
int originalWidth = originalImage.getWidth();
int originalHeight = originalImage.getHeight();
int left = (int)Math.floor(leftRatio * (double)originalWidth);
int top = (int)Math.floor(topRatio * (double)originalHeight);
int right = (int)Math.floor(rightRatio * (double)originalWidth);
int bottom = (int)Math.floor(bottomRatio * (double)originalHeight);
int cropWidth = Math.max(1, originalWidth - left - right);
int cropHeight = Math.max(1, originalHeight - top - bottom);
if (left < 0 || top < 0 || left + cropWidth > originalWidth || top + cropHeight > originalHeight) {
log.error("\u88c1\u526a\u533a\u57df\u65e0\u6548\u6216\u8d85\u51fa\u56fe\u7247\u8fb9\u754c");
}
return originalImage.getSubimage(left, top, cropWidth, cropHeight);
}
private BufferedImage rotateImage(BufferedImage originalImage, long rotation, long currentCx, Map<String, Object> result) {
int origWidth = originalImage.getWidth();
int origHeight = originalImage.getHeight();
if (origWidth <= 0 || origHeight <= 0) {
log.error("\u56fe\u7247\u533a\u57df\u65e0\u6548");
}
double emuPerPx = (double)currentCx / (double)origWidth;
if (currentCx == 0L) {
emuPerPx = 9525.0;
}
double degrees = (double)rotation / 60000.0;
double radians = Math.toRadians(degrees);
double sin = Math.abs(Math.sin(radians));
double cos = Math.abs(Math.cos(radians));
double TOLERANCE = 1.0E-10;
if (sin < 1.0E-10) {
sin = 0.0;
}
if (cos < 1.0E-10) {
cos = 0.0;
}
int newWidth = (int)Math.ceil((double)origWidth * cos + (double)origHeight * sin);
int newHeight = (int)Math.ceil((double)origWidth * sin + (double)origHeight * cos);
newWidth = Math.max(1, newWidth);
newHeight = Math.max(1, newHeight);
int superSampleFactor = 4;
int ssWidth = origWidth * superSampleFactor;
int ssHeight = origHeight * superSampleFactor;
int ssNewW = newWidth * superSampleFactor;
int ssNewH = newHeight * superSampleFactor;
int imgType1 = originalImage.getType() == 0 ? 2 : originalImage.getType();
BufferedImage highResOriginal = new BufferedImage(ssWidth, ssHeight, imgType1);
Graphics2D g2dHighRes = highResOriginal.createGraphics();
this.configureGraphics2D(g2dHighRes);
g2dHighRes.drawImage(originalImage, 0, 0, ssWidth, ssHeight, null);
g2dHighRes.dispose();
int imgType2 = highResOriginal.getType() == 0 ? 2 : highResOriginal.getType();
BufferedImage rotatedSuperSample = new BufferedImage(ssNewW, ssNewH, imgType2);
Graphics2D g2dRotated = rotatedSuperSample.createGraphics();
if (highResOriginal.getType() == 2) {
g2dRotated.setComposite(AlphaComposite.Clear);
g2dRotated.fillRect(0, 0, ssNewW, ssNewH);
g2dRotated.setComposite(AlphaComposite.SrcOver);
} else {
g2dRotated.setColor(Color.WHITE);
g2dRotated.fillRect(0, 0, ssNewW, ssNewH);
}
g2dRotated.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2dRotated.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2dRotated.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2dRotated.translate((double)ssNewW / 2.0, (double)ssNewH / 2.0);
g2dRotated.rotate(radians);
g2dRotated.drawImage((Image)highResOriginal, -ssWidth / 2, -ssHeight / 2, null);
g2dRotated.dispose();
BufferedImage rotatedImage = new BufferedImage(newWidth, newHeight, rotatedSuperSample.getType() == 0 ? 2 : rotatedSuperSample.getType());
Graphics2D g2dFinal = rotatedImage.createGraphics();
this.configureGraphics2D(g2dFinal);
g2dFinal.drawImage(rotatedSuperSample, 0, 0, newWidth, newHeight, null);
g2dFinal.dispose();
long newCx = Math.round((double)newWidth * emuPerPx);
long newCy = Math.round((double)newHeight * emuPerPx);
double newWidthPx = Math.round((double)newCx / 9525.0);
double newHeightPx = Math.round((double)newCy / 9525.0);
result.put("width", newWidthPx);
result.put("height", newHeightPx);
return rotatedImage;
}
private void configureGraphics2D(Graphics2D g2d) {
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
private int parseVmlCrop(String vmlCrop) {
if (vmlCrop == null || StringUtils.isBlank((CharSequence)vmlCrop)) {
return 0;
}
String clean = vmlCrop.trim().replaceAll("[fF]$", "");
try {
int value = Integer.parseInt(clean);
return (int)Math.round((double)value * 1.52587890625);
}
catch (NumberFormatException e) {
return 0;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
package com.wordparse.core;
/**
* Brand-neutral HTML attribute names (CSS properties and HTML attributes).
*
* Derived from jby AttrEnum — all tinymce / brand-specific entries removed.
*/
public enum HtmlAttr {
STYLE("style"),
COLOR("color"),
BACKGROUND_COLOR("background-color"),
BACKGROUND("background"),
FONT_WEIGHT("font-weight"),
FONT_SIZE("font-size"),
MIN_HEIGHT("min-height"),
LINE_HEIGHT("line-height"),
TEXT_ALIGN("text-align"),
PADDING("padding"),
PADDING_TOP("padding-top"),
PADDING_RIGHT("padding-right"),
PADDING_BOTTOM("padding-bottom"),
PADDING_LEFT("padding-left"),
TEXT_INDENT("text-indent"),
MARGIN("margin"),
MARGIN_TOP("margin-top"),
MARGIN_RIGHT("margin-right"),
MARGIN_BOTTOM("margin-bottom"),
MARGIN_LEFT("margin-left"),
VERTICAL_ALIGN("vertical-align"),
FONT_FAMILY("font-family"),
FONT_STYLE("font-style"),
FACE("face"),
SIZE("size"),
HREF("href"),
SRC("src"),
WIDTH("width"),
HEIGHT("height"),
CLASS("class"),
BORDER_STYLE("border-style"),
COLSPAN("colspan"),
ROWSPAN("rowspan"),
TEXT_DECORATION("text-decoration"),
BACKGROUND_IMAGE("background-image"),
TEXT_EMPHASIS_POSITION("text-emphasis-position"),
DATA_TYPE("data-type"),
SEGMENT("segment"),
LIST_STYLE_TYPE("list-style-type"),
ALT("alt"),
NAME("name"),
DATA_HREF("data-href"),
DATA_FILE_SUFFIX("data-file-suffix"),
BORDER("border"),
BORDER_LEFT("border-left"),
BORDER_BOTTOM("border-bottom"),
BORDER_RIGHT("border-right"),
BORDER_TOP("border-top"),
BORDER_LEFT_COLOR("border-left-color"),
BORDER_BOTTOM_COLOR("border-bottom-color"),
BORDER_RIGHT_COLOR("border-right-color"),
BORDER_TOP_COLOR("border-top-color");
private final String value;
HtmlAttr(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return value;
}
}

View File

@@ -0,0 +1,27 @@
package com.wordparse.core;
/**
* Brand-neutral HTML tag names.
*
* Minimal subset from jby TagEnum — only what the post-processing pipeline needs.
*/
public enum HtmlTag {
SUB("sub"),
SUP("sup");
private final String value;
HtmlTag(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return value;
}
}

View File

@@ -0,0 +1,14 @@
package com.wordparse.core;
public class ImageBO {
private String pictureUrl;
private Double width;
private Double height;
public String getPictureUrl() { return pictureUrl; }
public Double getWidth() { return width; }
public Double getHeight() { return height; }
public ImageBO setPictureUrl(String v) { this.pictureUrl = v; return this; }
public ImageBO setWidth(Double v) { this.width = v; return this; }
public ImageBO setHeight(Double v) { this.height = v; return this; }
}

View File

@@ -0,0 +1,19 @@
package com.wordparse.core;
import java.math.BigInteger;
public class NumPr {
private BigInteger start;
private BigInteger current;
private String numFmt;
private String lvlText;
public BigInteger getStart() { return start; }
public BigInteger getCurrent() { return current; }
public String getNumFmt() { return numFmt; }
public String getLvlText() { return lvlText; }
public void setStart(BigInteger v) { this.start = v; }
public void setCurrent(BigInteger v) { this.current = v; }
public void setNumFmt(String v) { this.numFmt = v; }
public void setLvlText(String v) { this.lvlText = v; }
}

View File

@@ -0,0 +1,68 @@
package com.wordparse.core;
import com.alibaba.fastjson2.JSONObject;
import javax.xml.transform.Transformer;
import java.nio.file.Path;
import java.util.*;
public class ParseBO {
private Path tempDir;
private String sourceFilePath;
private String htmlPath;
private String imagesDir;
private String ossFolder;
private Transformer transformer;
private final Map<String, String> openOfficeXmlMap = new HashMap<>();
private final Map<String, String> mathTypeMap = new HashMap<>();
private final Map<String, String> eqLatexMap = new HashMap<>();
private final Map<String, ImageBO> imageMap = new HashMap<>();
private final Map<String, String> imgMap = new HashMap<>();
private final Map<String, NumPr> numberingDefinitionsMap = new HashMap<>();
private List<JSONObject> latexReplaceList = new ArrayList<>();
private int count = 0;
private Boolean newParseVersion = false;
// Placeholder generators
public String nextOmmPlchld() { return "WP_OM_" + (++count) + "_END"; }
public String nextMtPlchld() { return "WP_MT_" + (++count) + "_END"; }
public String nextEqPlchld() { return "WP_EQ_" + (++count) + "_END"; }
public String nextImgName() { return "image_" + (++count); }
// Original named placeholder methods (called by finders)
public String getOpenOfficeXmlPlaceholder() { return nextOmmPlchld(); }
public String getMathTypePlaceholder(String rid) { return "WP_MT_" + rid + "_END"; }
public String getMathTypePlaceholder() { return nextMtPlchld(); }
public String getImagePlaceholder(String rid) { return "WP_IMG_" + rid + "_END"; }
public String getEqLatexPlaceholder() { return nextEqPlchld(); }
public String getShapeImgPlaceholder() { return "WP_IMG_" + (++count) + "_END"; }
// Getters
public Path getTempDir() { return tempDir; }
public String getSourceFilePath() { return sourceFilePath; }
public String getHtmlPath() { return htmlPath; }
public String getImagesDir() { return imagesDir; }
public String getOssFolder() { return ossFolder; }
public Transformer getTransformer() { return transformer; }
public Map<String, String> getOpenOfficeXmlMap() { return openOfficeXmlMap; }
public Map<String, String> getMathTypeMap() { return mathTypeMap; }
public Map<String, String> getEqLatexMap() { return eqLatexMap; }
public Map<String, ImageBO> getImageMap() { return imageMap; }
public Map<String, String> getImgMap() { return imgMap; }
public Map<String, NumPr> getNumberingDefinitionsMap() { return numberingDefinitionsMap; }
public int getCount() { return count; }
public Boolean getNewParseVersion() { return newParseVersion; }
public List<JSONObject> getLatexReplaceList() { return latexReplaceList; }
public Object getStyleDefinitionsPart() { return null; }
// Setters
public ParseBO setTempDir(Path v) { this.tempDir = v; return this; }
public ParseBO setSourceFilePath(String v) { this.sourceFilePath = v; return this; }
public ParseBO setHtmlPath(String v) { this.htmlPath = v; return this; }
public ParseBO setImagesDir(String v) { this.imagesDir = v; return this; }
public ParseBO setOssFolder(String v) { this.ossFolder = v; return this; }
public ParseBO setTransformer(Transformer v) { this.transformer = v; return this; }
public ParseBO setCount(int v) { this.count = v; return this; }
public ParseBO setNewParseVersion(Boolean v) { this.newParseVersion = v; return this; }
public ParseBO setLatexReplaceList(List<JSONObject> v) { this.latexReplaceList = v; return this; }
}

View File

@@ -0,0 +1,58 @@
package com.wordparse.core;
import java.io.Serializable;
/**
* Brand-neutral replacement for jby TagStyleBO.
*
* Represents a single CSS property (attr, value) parsed from an element's style attribute.
*/
public class TagStyleBO implements Serializable {
private static final long serialVersionUID = 1L;
private String tagName;
private String attr;
private String value;
public TagStyleBO() {
}
public TagStyleBO(String tagName, String attr, String value) {
this.tagName = tagName;
this.attr = attr;
this.value = value;
}
public String getTagName() {
return tagName;
}
public TagStyleBO setTagName(String tagName) {
this.tagName = tagName;
return this;
}
public String getAttr() {
return attr;
}
public TagStyleBO setAttr(String attr) {
this.attr = attr;
return this;
}
public String getValue() {
return value;
}
public TagStyleBO setValue(String value) {
this.value = value;
return this;
}
@Override
public String toString() {
return "TagStyleBO{tagName='" + tagName + "', attr='" + attr + "', value='" + value + "'}";
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,104 @@
package com.wordparse.oss;
import org.springframework.beans.factory.annotation.Value;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import jakarta.annotation.PostConstruct;
import java.io.File;
import java.io.InputStream;
import java.net.URI;
@Service
public class OssService {
private static final Logger log = LoggerFactory.getLogger(OssService.class);
@Value("${wordparse.oss.endpoint:https://fs.code-lab.cn}")
private String endpoint;
@Value("${wordparse.oss.access-key:}")
private String accessKey;
@Value("${wordparse.oss.secret-key:}")
private String secretKey;
@Value("${wordparse.oss.bucket:word-parse}")
private String bucket;
@Value("${wordparse.oss.prefix:office/file/parse/}")
private String prefix;
private S3Client client;
private boolean enabled = true;
@PostConstruct
public void init() {
try {
this.client = S3Client.builder()
.endpointOverride(URI.create(endpoint))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey)))
.region(Region.US_EAST_1)
.forcePathStyle(true)
.build();
// Quick connectivity test
client.headBucket(b -> b.bucket(bucket));
log.info("OSS client initialized: {} (bucket: {})", endpoint, bucket);
} catch (Exception e) {
enabled = false;
log.warn("OSS unavailable ({}: {}) — images will not be uploaded", e.getClass().getSimpleName(), e.getMessage());
}
}
public String uploadFile(File file, String folder, String fileName) {
if (!enabled) return null;
try {
String key = prefix + folder + "/" + fileName;
String contentType = guessContentType(fileName);
client.putObject(
PutObjectRequest.builder().bucket(bucket).key(key).contentType(contentType).build(),
RequestBody.fromFile(file));
return endpoint + "/" + bucket + "/" + key;
} catch (Exception e) {
log.warn("Upload failed: {}", fileName, e);
return null;
}
}
public String uploadStream(InputStream is, long size, String folder, String fileName) {
if (!enabled) return null;
try {
String key = prefix + folder + "/" + fileName;
String contentType = guessContentType(fileName);
client.putObject(
PutObjectRequest.builder().bucket(bucket).key(key).contentType(contentType).build(),
RequestBody.fromInputStream(is, size));
return endpoint + "/" + bucket + "/" + key;
} catch (Exception e) {
log.warn("Upload failed: {}", fileName, e);
return null;
}
}
private String guessContentType(String name) {
String ext = name.substring(name.lastIndexOf('.') + 1).toLowerCase();
return switch (ext) {
case "png" -> "image/png";
case "jpg", "jpeg" -> "image/jpeg";
case "gif" -> "image/gif";
case "svg" -> "image/svg+xml";
case "wmf" -> "image/wmf";
case "emf" -> "image/emf";
case "webp" -> "image/webp";
default -> "application/octet-stream";
};
}
}

View File

@@ -0,0 +1,851 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:mml="http://www.w3.org/1998/Math/MathML"
xmlns="http://www.w3.org/1998/Math/MathML"
exclude-result-prefixes="m w">
<xsl:output method="xml" indent="yes" encoding="UTF-8"/>
<!-- Root template -->
<xsl:template match="/">
<xsl:apply-templates select="//m:oMath"/>
</xsl:template>
<!-- Main math element -->
<xsl:template match="m:oMath">
<math>
<xsl:apply-templates select="*"/>
</math>
</xsl:template>
<!-- Text run -->
<xsl:template match="m:r">
<xsl:apply-templates select="m:t"/>
</xsl:template>
<!-- Text content -->
<xsl:template match="m:t">
<!-- Get style from parent <m:r><m:rPr><m:sty> -->
<xsl:variable name="sty" select="../m:rPr/m:sty/@m:val"/>
<!-- Get Word run properties (w:rPr) - check for italic (w:i) -->
<!-- w:rPr is a sibling of m:t (both are children of m:r) -->
<!-- CRITICAL: w:i with @w:val="0" or "false" means NOT italic! -->
<!-- Only treat as italic if w:i exists AND val is not "0" or "false" -->
<xsl:variable name="has_word_italic" select="boolean(../w:rPr/w:i[not(@w:val = '0') and not(@w:val = 'false')])"/>
<!-- Get math "normal" style (m:nor) - text mode button -->
<xsl:variable name="has_math_normal" select="boolean(../m:rPr/m:nor)"/>
<xsl:choose>
<xsl:when test="normalize-space(.) = ''">
<!-- Skip empty text -->
</xsl:when>
<xsl:when test="number(.) = number(.)">
<mn><xsl:value-of select="normalize-space(.)"/></mn>
</xsl:when>
<!-- Fence delimiters: parentheses, brackets, pipes (with fence="true") -->
<xsl:when test=". = '(' or . = ')' or . = '[' or . = ']' or . = '|'">
<mo fence="true"><xsl:value-of select="normalize-space(.)"/></mo>
</xsl:when>
<!-- Plain operators (no fence attribute) -->
<!-- NOTE: { and } are converted to markers to distinguish source braces from generated LaTeX braces -->
<xsl:when test=". = '{' or . = '}' or . = '+' or . = '-' or . = '=' or . = '&lt;' or . = '&gt;' or . = '±' or . = '≠' or . = '≤' or . = '≥' or . = '×' or . = '÷' or . = '∞'">
<mo>
<xsl:choose>
<xsl:when test=". = '{'">__LBRACE__</xsl:when>
<xsl:when test=". = '}'">__RBRACE__</xsl:when>
<xsl:otherwise><xsl:value-of select="normalize-space(.)"/></xsl:otherwise>
</xsl:choose>
</mo>
</xsl:when>
<!-- Mixed operators/delimiters (like ")+" or ")=" or "(A" or "={" or ",}") -->
<!-- Check if string contains multiple operator/delimiter characters -->
<xsl:when test="string-length(normalize-space(.)) &gt; 1 and (contains(., '(') or contains(., ')') or contains(., '[') or contains(., ']') or contains(., '{') or contains(., '}') or contains(., '+') or contains(., '-') or contains(., '=') or contains(., '&lt;') or contains(., '&gt;'))">
<!-- Split into individual characters and classify each one -->
<!-- Pass style info so letters get proper mathvariant (e.g., mathvariant="normal" for m:nor text mode) -->
<xsl:call-template name="split-operators">
<xsl:with-param name="text" select="normalize-space(.)"/>
<xsl:with-param name="math_variant">
<xsl:choose>
<xsl:when test="$has_word_italic">italic</xsl:when>
<xsl:when test="$sty = 'b' or $sty = 'bi'">bold</xsl:when>
<xsl:when test="$sty = 'p'">normal</xsl:when>
<xsl:when test="$has_math_normal">normal</xsl:when>
<xsl:otherwise></xsl:otherwise>
</xsl:choose>
</xsl:with-param>
</xsl:call-template>
</xsl:when>
<xsl:when test="contains(., '|')">
<!-- Special handling for text containing | (like "x|x", "A|B", or "||") -->
<!-- Split by | and create separate elements with fence="true" for absolute values -->
<xsl:variable name="before" select="substring-before(., '|')"/>
<xsl:variable name="after" select="substring-after(., '|')"/>
<!-- Process before part (plain text) -->
<xsl:if test="$before != ''">
<xsl:choose>
<!-- If before part contains operators or is multi-char, use split-operators -->
<xsl:when test="string-length($before) &gt; 1">
<xsl:call-template name="split-operators">
<xsl:with-param name="text" select="$before"/>
</xsl:call-template>
</xsl:when>
<!-- Single character - output as mi -->
<xsl:otherwise>
<mi><xsl:value-of select="$before"/></mi>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
<!-- Output the | as fence for potential absolute value -->
<mo fence="true">|</mo>
<!-- Process after part recursively if it contains more | or operators -->
<xsl:if test="$after != ''">
<xsl:choose>
<!-- If after part is just |, output as fence -->
<xsl:when test="$after = '|'">
<mo fence="true">|</mo>
</xsl:when>
<!-- If after part contains more |, recursively process this m:t content -->
<xsl:when test="contains($after, '|')">
<xsl:call-template name="process-text-with-pipe">
<xsl:with-param name="text" select="$after"/>
</xsl:call-template>
</xsl:when>
<!-- If after part contains operators, use split-operators -->
<xsl:when test="string-length($after) &gt; 1 and (contains($after, '+') or contains($after, '-') or contains($after, '='))">
<xsl:call-template name="split-operators">
<xsl:with-param name="text" select="$after"/>
</xsl:call-template>
</xsl:when>
<!-- Single character or simple text - output as mi -->
<xsl:otherwise>
<mi><xsl:value-of select="$after"/></mi>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:when>
<xsl:when test="string-length(normalize-space(.)) = 1 and not(contains('0123456789', .))">
<mi>
<!-- Apply mathvariant based on style -->
<!-- Note: m:sty='i' (italic) is NOT handled here as italic is the default for variables -->
<!-- Priority: italic (w:i explicit) > bold > plain (m:sty='p') > normal (m:nor) -->
<xsl:choose>
<xsl:when test="$has_word_italic">
<xsl:attribute name="mathvariant">italic</xsl:attribute>
</xsl:when>
<xsl:when test="$sty = 'b' or $sty = 'bi'">
<xsl:attribute name="mathvariant">bold</xsl:attribute>
</xsl:when>
<xsl:when test="$sty = 'p'">
<xsl:attribute name="mathvariant">normal</xsl:attribute>
</xsl:when>
<xsl:when test="$has_math_normal">
<xsl:attribute name="mathvariant">normal</xsl:attribute>
</xsl:when>
</xsl:choose>
<xsl:value-of select="normalize-space(.)"/>
</mi>
</xsl:when>
<xsl:otherwise>
<mi>
<!-- Apply mathvariant for multi-character identifiers too -->
<!-- Note: m:sty='i' (italic) is NOT handled here as italic is the default for variables -->
<!-- Priority: italic (w:i explicit) > bold > plain (m:sty='p') > normal (m:nor) -->
<xsl:choose>
<xsl:when test="$has_word_italic">
<xsl:attribute name="mathvariant">italic</xsl:attribute>
</xsl:when>
<xsl:when test="$sty = 'b' or $sty = 'bi'">
<xsl:attribute name="mathvariant">bold</xsl:attribute>
</xsl:when>
<xsl:when test="$sty = 'p'">
<xsl:attribute name="mathvariant">normal</xsl:attribute>
</xsl:when>
<xsl:when test="$has_math_normal">
<xsl:attribute name="mathvariant">normal</xsl:attribute>
</xsl:when>
</xsl:choose>
<xsl:value-of select="normalize-space(.)"/>
</mi>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- Superscript -->
<xsl:template match="m:sSup">
<msup>
<xsl:apply-templates select="m:e"/>
<xsl:apply-templates select="m:sup"/>
</msup>
</xsl:template>
<!-- Subscript -->
<xsl:template match="m:sSub">
<msub>
<xsl:apply-templates select="m:e"/>
<xsl:apply-templates select="m:sub"/>
</msub>
</xsl:template>
<!-- Sub-superscript -->
<xsl:template match="m:sSubSup">
<msubsup>
<xsl:apply-templates select="m:e"/>
<xsl:apply-templates select="m:sub"/>
<xsl:apply-templates select="m:sup"/>
</msubsup>
</xsl:template>
<!-- Pre-script (left-side subscript/superscript) -->
<!-- Used for nuclear notation like _{2}^{3}He -->
<xsl:template match="m:sPre">
<mmultiscripts>
<xsl:apply-templates select="m:e"/>
<mprescripts/>
<xsl:apply-templates select="m:sub"/>
<xsl:apply-templates select="m:sup"/>
</mmultiscripts>
</xsl:template>
<!-- Fraction -->
<!-- m:type values: bar (default), noBar, lin (linear), skw (skewed) -->
<xsl:template match="m:f">
<xsl:variable name="type" select="m:fPr/m:type/@m:val"/>
<mfrac>
<!-- Add linethickness="0" for noBar fractions (stacked without bar) -->
<xsl:if test="$type = 'noBar'">
<xsl:attribute name="linethickness">0</xsl:attribute>
</xsl:if>
<!-- Add bevelled="true" for linear fractions -->
<xsl:if test="$type = 'lin'">
<xsl:attribute name="bevelled">true</xsl:attribute>
</xsl:if>
<xsl:apply-templates select="m:num"/>
<xsl:apply-templates select="m:den"/>
</mfrac>
</xsl:template>
<!-- Radical -->
<xsl:template match="m:rad">
<xsl:choose>
<xsl:when test="m:deg">
<mroot>
<xsl:apply-templates select="m:e"/>
<xsl:apply-templates select="m:deg"/>
</mroot>
</xsl:when>
<xsl:otherwise>
<msqrt>
<xsl:apply-templates select="m:e"/>
</msqrt>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- Parentheses/delimiters -->
<xsl:template match="m:d">
<mrow>
<!-- Opening delimiter: skip if begChr has empty or whitespace-only val -->
<xsl:choose>
<xsl:when test="not(m:dPr/m:begChr)">
<mo fence="true">(</mo>
</xsl:when>
<xsl:when test="normalize-space(m:dPr/m:begChr/@m:val) != ''">
<mo fence="true">
<xsl:value-of select="m:dPr/m:begChr/@m:val"/>
</mo>
</xsl:when>
<!-- begChr with empty/whitespace val → no opening delimiter -->
</xsl:choose>
<!-- Handle multiple m:e elements with separators -->
<xsl:for-each select="m:e">
<xsl:apply-templates select="."/>
<!-- Insert separator | between elements (except after the last one) -->
<xsl:if test="position() != last()">
<mo>|</mo>
</xsl:if>
</xsl:for-each>
<!-- Closing delimiter: skip if endChr has empty or whitespace-only val -->
<xsl:choose>
<xsl:when test="not(m:dPr/m:endChr)">
<mo fence="true">)</mo>
</xsl:when>
<xsl:when test="normalize-space(m:dPr/m:endChr/@m:val) != ''">
<mo fence="true">
<xsl:value-of select="m:dPr/m:endChr/@m:val"/>
</mo>
</xsl:when>
<!-- endChr with empty/whitespace val → no closing delimiter -->
</xsl:choose>
</mrow>
</xsl:template>
<!-- Function -->
<xsl:template match="m:func">
<mrow>
<xsl:apply-templates select="m:fName"/>
<xsl:apply-templates select="m:e"/>
</mrow>
</xsl:template>
<!-- Matrix -->
<xsl:template match="m:m">
<mtable>
<xsl:apply-templates select="m:mr"/>
</mtable>
</xsl:template>
<!-- Matrix row -->
<xsl:template match="m:mr">
<mtr>
<xsl:apply-templates select="m:e"/>
</mtr>
</xsl:template>
<!-- Equation array (for piecewise functions) -->
<xsl:template match="m:eqArr">
<mtable>
<!-- Each <m:e> child represents one row in the piecewise function -->
<xsl:for-each select="m:e">
<mtr>
<mtd>
<xsl:apply-templates select="node()"/>
</mtd>
</mtr>
</xsl:for-each>
</mtable>
</xsl:template>
<!-- Bar (overbar/underbar) -->
<!-- OMML spec: no pos attribute = underline (default), pos="top" = overline, pos="bot" = underline -->
<xsl:template match="m:bar">
<xsl:choose>
<xsl:when test="m:barPr/m:pos/@m:val = 'top'">
<mover>
<xsl:apply-templates select="m:e"/>
<mo>‾</mo>
</mover>
</xsl:when>
<xsl:otherwise>
<!-- Default: no pos attribute or pos="bot" both mean underline -->
<munder>
<xsl:apply-templates select="m:e"/>
<mo>‾</mo>
</munder>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- Limits -->
<!-- limLow: Lower limit position (content BELOW the base, like underscript) -->
<xsl:template match="m:limLow">
<munder>
<xsl:apply-templates select="m:e"/>
<xsl:apply-templates select="m:lim"/>
</munder>
</xsl:template>
<!-- limUpp: Upper limit position (content ABOVE the base, like overscript) -->
<xsl:template match="m:limUpp">
<mover>
<xsl:apply-templates select="m:e"/>
<xsl:apply-templates select="m:lim"/>
</mover>
</xsl:template>
<xsl:template match="m:limUppLow">
<munderover>
<xsl:apply-templates select="m:e"/>
<xsl:apply-templates select="m:lim[1]"/>
<xsl:apply-templates select="m:lim[2]"/>
</munderover>
</xsl:template>
<!-- N-ary operators (integrals, sums, products) -->
<xsl:template match="m:nary">
<xsl:variable name="chr" select="m:naryPr/m:chr/@m:val"/>
<xsl:variable name="limLoc" select="m:naryPr/m:limLoc/@m:val"/>
<xsl:variable name="subHide" select="m:naryPr/m:subHide/@m:val"/>
<xsl:variable name="supHide" select="m:naryPr/m:supHide/@m:val"/>
<xsl:choose>
<!-- Both limits hidden: just operator and integrand -->
<xsl:when test="$subHide = '1' and $supHide = '1'">
<mrow>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>∫</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="m:e"/>
</mrow>
</xsl:when>
<!-- Limits as subscript/superscript (default for integrals) -->
<xsl:when test="not($limLoc) or $limLoc = 'subSup'">
<xsl:choose>
<!-- Both sub and sup present -->
<xsl:when test="m:sub and m:sup">
<msubsup>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>∫</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="m:sub"/>
<xsl:apply-templates select="m:sup"/>
</msubsup>
<xsl:apply-templates select="m:e"/>
</xsl:when>
<!-- Only subscript -->
<xsl:when test="m:sub">
<msub>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>∫</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="m:sub"/>
</msub>
<xsl:apply-templates select="m:e"/>
</xsl:when>
<!-- Only superscript -->
<xsl:when test="m:sup">
<msup>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>∫</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="m:sup"/>
</msup>
<xsl:apply-templates select="m:e"/>
</xsl:when>
<!-- No limits -->
<xsl:otherwise>
<mrow>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>∫</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="m:e"/>
</mrow>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<!-- Limits as under/over (for sums, products) -->
<xsl:when test="$limLoc = 'undOvr'">
<mrow>
<xsl:choose>
<!-- Both under and over -->
<xsl:when test="m:sub and m:sup and not($subHide = '1') and not($supHide = '1')">
<munderover>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>∫</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="m:sub"/>
<xsl:apply-templates select="m:sup"/>
</munderover>
</xsl:when>
<!-- Only under -->
<xsl:when test="m:sub and not($subHide = '1')">
<munder>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>∫</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="m:sub"/>
</munder>
</xsl:when>
<!-- Only over -->
<xsl:when test="m:sup and not($supHide = '1')">
<mover>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>∫</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="m:sup"/>
</mover>
</xsl:when>
<!-- No visible limits -->
<xsl:otherwise>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>∫</xsl:otherwise>
</xsl:choose>
</mo>
</xsl:otherwise>
</xsl:choose>
<xsl:apply-templates select="m:e"/>
</mrow>
</xsl:when>
</xsl:choose>
</xsl:template>
<!-- Accent (vector arrows, dots, etc.) -->
<xsl:template match="m:acc">
<xsl:variable name="chr" select="m:accPr/m:chr/@m:val"/>
<mover>
<!-- Add data attribute to distinguish default accents from explicit ones -->
<xsl:if test="not($chr)">
<xsl:attribute name="data-default-accent">true</xsl:attribute>
</xsl:if>
<xsl:apply-templates select="m:e"/>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>⃗</xsl:otherwise><!-- Default accent (should be hat in most cases) -->
</xsl:choose>
</mo>
</mover>
</xsl:template>
<!-- Group character (for over/under arrows with text) -->
<xsl:template match="m:groupChr">
<xsl:variable name="chr" select="m:groupChrPr/m:chr/@m:val"/>
<xsl:variable name="pos" select="m:groupChrPr/m:pos/@m:val"/>
<xsl:variable name="vertJc" select="m:groupChrPr/m:vertJc/@m:val"/>
<xsl:choose>
<!-- vertJc='bot': Character at bottom (base), content above → mover(char, content) -->
<!-- This is used for extensible arrows like \xrightarrow[]{text} or \overset{text}{arrow} -->
<xsl:when test="$vertJc = 'bot'">
<mover accent="false">
<mo stretchy="true">
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>→</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="m:e"/>
</mover>
</xsl:when>
<!-- pos='bot': Character position at bottom, content above → munder -->
<xsl:when test="$pos = 'bot'">
<munder>
<xsl:apply-templates select="m:e"/>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>⏟</xsl:otherwise><!-- Default underbrace -->
</xsl:choose>
</mo>
</munder>
</xsl:when>
<!-- pos='top' (or default): Character above content → mover(content, char) -->
<xsl:otherwise>
<mover>
<xsl:apply-templates select="m:e"/>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>⏞</xsl:otherwise><!-- Default overbrace -->
</xsl:choose>
</mo>
</mover>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- Box (but detect if it contains groupChr for reaction arrows) -->
<xsl:template match="m:box">
<xsl:choose>
<!-- Skip boxes with opEmu="1" (operator emulation - should be transparent) -->
<!-- This means process all children in m:e transparently -->
<xsl:when test="m:boxPr/m:opEmu/@m:val='1'">
<xsl:apply-templates select="m:e/*"/>
</xsl:when>
<!-- If box contains ONLY a single direct groupChr, handle specially -->
<!-- This is for cases like reaction arrows: text above/below extensible arrow -->
<xsl:when test="count(m:e/*) = 1 and m:e/m:groupChr">
<xsl:variable name="groupChr" select="m:e/m:groupChr"/>
<xsl:variable name="arrow" select="$groupChr/m:groupChrPr/m:chr/@m:val"/>
<xsl:variable name="vertJc" select="$groupChr/m:groupChrPr/m:vertJc/@m:val"/>
<xsl:variable name="pos" select="$groupChr/m:groupChrPr/m:pos/@m:val"/>
<xsl:choose>
<xsl:when test="$vertJc = 'bot'">
<!-- Arrow at bottom with text above (like \xrightarrow[]{text}) -->
<!-- For extensible arrows: mover with arrow base, text above -->
<mover accent="false">
<mo stretchy="true">
<xsl:choose>
<xsl:when test="$arrow"><xsl:value-of select="$arrow"/></xsl:when>
<xsl:otherwise>→</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="$groupChr/m:e"/>
</mover>
</xsl:when>
<xsl:when test="$pos = 'top'">
<!-- Arrow at top: \overrightarrow pattern -->
<!-- mover with content base, arrow above -->
<mover>
<xsl:apply-templates select="$groupChr/m:e"/>
<mo>
<xsl:choose>
<xsl:when test="$arrow"><xsl:value-of select="$arrow"/></xsl:when>
<xsl:otherwise>→</xsl:otherwise>
</xsl:choose>
</mo>
</mover>
</xsl:when>
<xsl:otherwise>
<!-- Default: Vector arrow over content -->
<mover>
<xsl:apply-templates select="$groupChr/m:e"/>
<mo>
<xsl:choose>
<xsl:when test="$arrow"><xsl:value-of select="$arrow"/></xsl:when>
<xsl:otherwise>→</xsl:otherwise>
</xsl:choose>
</mo>
</mover>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<!-- Skip phantom/layout boxes (argSz with negative value) -->
<!-- These are used for sizing/spacing control, not actual visible boxes -->
<xsl:when test="m:e/m:argPr/m:argSz/@m:val &lt; 0">
<xsl:apply-templates select="m:e"/>
</xsl:when>
<!-- Regular box -->
<xsl:otherwise>
<menclose notation="box">
<xsl:apply-templates select="m:e"/>
</menclose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- Border box with strikethrough and border notation support -->
<xsl:template match="m:borderBox">
<xsl:variable name="props" select="m:borderBoxPr"/>
<!-- Check for strikethrough properties -->
<xsl:variable name="strikeBLTR" select="boolean($props/m:strikeBLTR)"/>
<xsl:variable name="strikeTLBR" select="boolean($props/m:strikeTLBR)"/>
<xsl:variable name="strikeH" select="boolean($props/m:strikeH)"/>
<xsl:variable name="strikeV" select="boolean($props/m:strikeV)"/>
<!-- Check for border hiding properties -->
<xsl:variable name="hideTop" select="boolean($props/m:hideTop)"/>
<xsl:variable name="hideBot" select="boolean($props/m:hideBot)"/>
<xsl:variable name="hideLeft" select="boolean($props/m:hideLeft)"/>
<xsl:variable name="hideRight" select="boolean($props/m:hideRight)"/>
<!-- Determine notation attribute -->
<xsl:variable name="notation">
<xsl:choose>
<!-- Combined diagonal strikes → xcancel -->
<xsl:when test="$strikeBLTR and $strikeTLBR">updiagonalstrike downdiagonalstrike</xsl:when>
<!-- Single diagonal strike BLTR → cancel (updiagonal) -->
<xsl:when test="$strikeBLTR">updiagonalstrike</xsl:when>
<!-- Single diagonal strike TLBR → bcancel (downdiagonal) -->
<xsl:when test="$strikeTLBR">downdiagonalstrike</xsl:when>
<!-- Combined horizontal and vertical → cancel -->
<xsl:when test="$strikeH and $strikeV">horizontalstrike verticalstrike</xsl:when>
<!-- Single horizontal strike → cancel -->
<xsl:when test="$strikeH">horizontalstrike</xsl:when>
<!-- Single vertical strike → bcancel -->
<xsl:when test="$strikeV">verticalstrike</xsl:when>
<!-- Single side visible -->
<xsl:when test="not($hideTop) and $hideBot and $hideLeft and $hideRight">top</xsl:when>
<xsl:when test="$hideTop and not($hideBot) and $hideLeft and $hideRight">bottom</xsl:when>
<xsl:when test="$hideTop and $hideBot and not($hideLeft) and $hideRight">left</xsl:when>
<xsl:when test="$hideTop and $hideBot and $hideLeft and not($hideRight)">right</xsl:when>
<!-- Two sides visible -->
<xsl:when test="not($hideTop) and not($hideBot) and $hideLeft and $hideRight">top bottom</xsl:when>
<xsl:when test="not($hideTop) and $hideBot and not($hideLeft) and $hideRight">top left</xsl:when>
<xsl:when test="not($hideTop) and $hideBot and $hideLeft and not($hideRight)">top right</xsl:when>
<xsl:when test="$hideTop and not($hideBot) and not($hideLeft) and $hideRight">bottom left</xsl:when>
<xsl:when test="$hideTop and not($hideBot) and $hideLeft and not($hideRight)">bottom right</xsl:when>
<xsl:when test="$hideTop and $hideBot and not($hideLeft) and not($hideRight)">left right</xsl:when>
<!-- Three sides visible -->
<xsl:when test="not($hideTop) and not($hideBot) and not($hideLeft) and $hideRight">top bottom left</xsl:when>
<xsl:when test="not($hideTop) and not($hideBot) and $hideLeft and not($hideRight)">top bottom right</xsl:when>
<xsl:when test="not($hideTop) and $hideBot and not($hideLeft) and not($hideRight)">top left right</xsl:when>
<xsl:when test="$hideTop and not($hideBot) and not($hideLeft) and not($hideRight)">bottom left right</xsl:when>
<!-- All four sides visible (or none hidden) -->
<xsl:when test="not($hideTop or $hideBot or $hideLeft or $hideRight)">box</xsl:when>
<!-- Default: roundedbox -->
<xsl:otherwise>roundedbox</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<menclose notation="{$notation}">
<xsl:apply-templates select="m:e"/>
</menclose>
</xsl:template>
<!-- Math element base -->
<xsl:template match="m:e | m:num | m:den | m:sup | m:sub | m:deg | m:fName | m:lim">
<mrow>
<xsl:apply-templates select="*"/>
</mrow>
</xsl:template>
<!-- Template to split mixed operator/delimiter strings character by character -->
<!-- Recursive template to process text containing | characters -->
<xsl:template name="process-text-with-pipe">
<xsl:param name="text"/>
<xsl:if test="string-length($text) &gt; 0">
<xsl:choose>
<!-- If text contains |, split and recurse -->
<xsl:when test="contains($text, '|')">
<xsl:variable name="before" select="substring-before($text, '|')"/>
<xsl:variable name="after" select="substring-after($text, '|')"/>
<!-- Output before part if not empty -->
<xsl:if test="$before != ''">
<xsl:choose>
<!-- If before is single char, output as mi or mn -->
<xsl:when test="string-length($before) = 1">
<xsl:choose>
<xsl:when test="contains('0123456789', $before)">
<mn><xsl:value-of select="$before"/></mn>
</xsl:when>
<xsl:otherwise>
<mi><xsl:value-of select="$before"/></mi>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<!-- Multi-char - use split-operators -->
<xsl:otherwise>
<xsl:call-template name="split-operators">
<xsl:with-param name="text" select="$before"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
<!-- Output | as fence -->
<mo fence="true">|</mo>
<!-- Recurse on after part -->
<xsl:if test="$after != ''">
<xsl:call-template name="process-text-with-pipe">
<xsl:with-param name="text" select="$after"/>
</xsl:call-template>
</xsl:if>
</xsl:when>
<!-- No more | in text - output as-is -->
<xsl:otherwise>
<xsl:choose>
<!-- Single char -->
<xsl:when test="string-length($text) = 1">
<xsl:choose>
<xsl:when test="contains('0123456789', $text)">
<mn><xsl:value-of select="$text"/></mn>
</xsl:when>
<xsl:otherwise>
<mi><xsl:value-of select="$text"/></mi>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<!-- Multi-char - use split-operators for proper handling -->
<xsl:otherwise>
<xsl:call-template name="split-operators">
<xsl:with-param name="text" select="$text"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:template>
<xsl:template name="split-operators">
<xsl:param name="text"/>
<!-- Style variant to apply to identifier (letter) elements: 'normal', 'bold', 'italic', or empty -->
<xsl:param name="math_variant" select="''"/>
<xsl:if test="string-length($text) &gt; 0">
<!-- Process first character -->
<xsl:variable name="char" select="substring($text, 1, 1)"/>
<xsl:choose>
<!-- Fence delimiters with fence="true" -->
<xsl:when test="$char = '(' or $char = ')' or $char = '[' or $char = ']' or $char = '|'">
<mo fence="true"><xsl:value-of select="$char"/></mo>
</xsl:when>
<!-- Plain operators (no fence attribute) -->
<!-- NOTE: { and } are converted to markers to distinguish source braces from generated LaTeX braces -->
<xsl:when test="$char = '+' or $char = '-' or $char = '=' or $char = '&lt;' or $char = '&gt;' or $char = '*' or $char = '/' or $char = '±' or $char = '≠' or $char = '≤' or $char = '≥' or $char = '×' or $char = '÷'">
<mo><xsl:value-of select="$char"/></mo>
</xsl:when>
<!-- Source braces - use markers -->
<xsl:when test="$char = '{'">
<mo>__LBRACE__</mo>
</xsl:when>
<xsl:when test="$char = '}'">
<mo>__RBRACE__</mo>
</xsl:when>
<!-- Numbers -->
<xsl:when test="contains('0123456789', $char)">
<mn><xsl:value-of select="$char"/></mn>
</xsl:when>
<!-- Identifiers (letters) -->
<xsl:otherwise>
<mi>
<!-- Apply mathvariant from parent run style if available -->
<xsl:if test="$math_variant != ''">
<xsl:attribute name="mathvariant"><xsl:value-of select="$math_variant"/></xsl:attribute>
</xsl:if>
<xsl:value-of select="$char"/>
</mi>
</xsl:otherwise>
</xsl:choose>
<!-- Recursively process remaining characters -->
<xsl:if test="string-length($text) &gt; 1">
<xsl:call-template name="split-operators">
<xsl:with-param name="text" select="substring($text, 2)"/>
<xsl:with-param name="math_variant" select="$math_variant"/>
</xsl:call-template>
</xsl:if>
</xsl:if>
</xsl:template>
<!-- Properties elements - ignore -->
<xsl:template match="m:rPr | m:ctrlPr | m:dPr | m:fPr | m:funcPr | m:limLowPr | m:limUppPr | m:naryPr | m:phantPr | m:radPr | m:sPrePr | m:sSubPr | m:sSubSupPr | m:sSupPr | m:accPr | m:barPr | m:boxPr | m:borderBoxPr | m:dPr | m:eqArrPr | m:groupChrPr | m:mPr | w:rPr">
<!-- Ignore formatting properties -->
</xsl:template>
<!-- Control properties - ignore -->
<xsl:template match="m:ctrlPr">
<!-- Ignore control properties -->
</xsl:template>
<!-- Default template -->
<xsl:template match="*">
<xsl:apply-templates select="*"/>
</xsl:template>
</xsl:stylesheet>

View File

@@ -0,0 +1,22 @@
spring:
application:
name: word-file-parse
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
server:
port: 8080
wordparse:
tmp-path: ${java.io.tmpdir}
image-scaling-factor: 2.0
transform-scripts-path: /opt/transform
python-command: python3
oss:
endpoint: https://fs.code-lab.cn
access-key: ${WORDPARSE_OSS_AK:}
secret-key: ${WORDPARSE_OSS_SK:}
bucket: word-parse
prefix: office/file/parse/

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<svg xmlns="http://www.w3.org/2000/svg" width="1.385ex" height="2.262ex" viewBox="0 -750 612 1000" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" style=""><defs><path id="MJX-39-TEX-N-2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path><path id="MJX-39-TEX-N-5B" d="M118 -250V750H255V710H158V-210H255V-250H118Z"></path></defs><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="matrix(1 0 0 -1 0 0)"><g data-mml-node="math"><g data-mml-node="mo"><use xlink:href="#MJX-39-TEX-N-2212"></use></g><g data-mml-node="mspace" transform="translate(778, 0)"></g><g data-mml-node="mo" transform="translate(334, 0)"><use xlink:href="#MJX-39-TEX-N-5B"></use></g></g></g></svg>

After

Width:  |  Height:  |  Size: 789 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<svg xmlns="http://www.w3.org/2000/svg" width="2.204ex" height="2.262ex" viewBox="0 -750 974.3 1000" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" style=""><defs><path id="MJX-94-TEX-N-2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path><path id="MJX-94-TEX-N-5D" d="M22 710V750H159V-250H22V-210H119V710H22Z"></path><path id="MJX-94-TEX-I-1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z"></path></defs><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="matrix(1 0 0 -1 0 0)"><g data-mml-node="math"><g data-mml-node="mo"><use xlink:href="#MJX-94-TEX-N-2212"></use></g><g data-mml-node="mspace" transform="translate(778, 0)"></g><g data-mml-node="msub" transform="translate(222, 0)"><g data-mml-node="mo"><use xlink:href="#MJX-94-TEX-N-5D"></use></g><g data-mml-node="mi" transform="translate(278, -150) scale(0.707)"><use xlink:href="#MJX-94-TEX-I-1D45B"></use></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<svg xmlns="http://www.w3.org/2000/svg" width="2.649ex" height="2.262ex" viewBox="0 -750 1170.8 1000" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" style=""><defs><path id="MJX-92-TEX-N-2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path><path id="MJX-92-TEX-N-5D" d="M22 710V750H159V-250H22V-210H119V710H22Z"></path><path id="MJX-92-TEX-I-1D45A" d="M21 287Q22 293 24 303T36 341T56 388T88 425T132 442T175 435T205 417T221 395T229 376L231 369Q231 367 232 367L243 378Q303 442 384 442Q401 442 415 440T441 433T460 423T475 411T485 398T493 385T497 373T500 364T502 357L510 367Q573 442 659 442Q713 442 746 415T780 336Q780 285 742 178T704 50Q705 36 709 31T724 26Q752 26 776 56T815 138Q818 149 821 151T837 153Q857 153 857 145Q857 144 853 130Q845 101 831 73T785 17T716 -10Q669 -10 648 17T627 73Q627 92 663 193T700 345Q700 404 656 404H651Q565 404 506 303L499 291L466 157Q433 26 428 16Q415 -11 385 -11Q372 -11 364 -4T353 8T350 18Q350 29 384 161L420 307Q423 322 423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 181Q151 335 151 342Q154 357 154 369Q154 405 129 405Q107 405 92 377T69 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></defs><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="matrix(1 0 0 -1 0 0)"><g data-mml-node="math"><g data-mml-node="mo"><use xlink:href="#MJX-92-TEX-N-2212"></use></g><g data-mml-node="mspace" transform="translate(778, 0)"></g><g data-mml-node="msub" transform="translate(222, 0)"><g data-mml-node="mo"><use xlink:href="#MJX-92-TEX-N-5D"></use></g><g data-mml-node="mi" transform="translate(278, -150) scale(0.707)"><use xlink:href="#MJX-92-TEX-I-1D45A"></use></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB