Files
DoLuEdu-WordFileParse/java-src/utils/finder/DrawingPictFinder.java
Quentin 3271fee5cc init: WordFileParse docx→HTML 解析服务(含 Word EQ 域公式修复)
服务:Spring Boot 3.2 + docx4j 8.3.9,POST /api/v3/server_office_file/file/parse
      接受 .docx,返回 HTML(公式 → LaTeX,图片 → RustFS 外链)

公式支持三类来源:
- OMML(Word 2007+ 公式)      → Python omml_mathml2latex
- MathType OLE 对象            → Ruby mathtype2mathml → MathML → LaTeX
- Word EQ 域(老教辅主力)      → EqToLatexUtil(本次新增)

本次改动:
1. 新增 java-svc/.../core/EqToLatexUtil.java:
   移植自 jby backend-services office-core,EQ 语法 → LaTeX
   (\f→\dfrac、\r→\sqrt、\i→\int/\sum/\prod、\s→^{}/_{}、\b→\left\right、
     \a→array、\x→\boxed/\overline、\o\al 堆叠、圈码、化学符号、极限、连等式)
   品牌标记 JBYU* → WPU*,与采集端一致
2. CtoMathFinder:接上 EQ 分支(原为 not-ported 桩),域代码经
   EqToLatexUtil.convert 转 LaTeX 写入 eqLatexMap(WPEQNUM 占位符)
3. WordParseUtil.dealPlaceholder:EQ 的 LaTeX 同样加 \( \) 包裹
   (原先因 WPEQNUM 例外不加包裹,前端/MathJax 不渲染)

修前症状:教辅 Word 里分式/方程组/上下标整段丢失(如 {−1/3, 1} 变成 {-,1})
修后实测:46 份教辅 2908 处 EQ 域全部还原,公式总数 2954 处、零未包裹残留

安全:application.yml(含 RustFS 密钥)不入库,提供 application.yml.example;
      OssService 的 @Value 默认密钥已清空
2026-09-29 09:43:13 +08:00

435 lines
22 KiB
Java

/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* com.alibaba.excel.util.StringUtils
* com.jby.common.util.UniqueUtil
* 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.jby.officecore.utils.finder;
import com.alibaba.excel.util.StringUtils;
import com.jby.common.util.UniqueUtil;
import com.jby.officecore.domain.bo.ImageBO;
import com.jby.officecore.domain.bo.ParseBO;
import com.jby.officecore.enums.AttrEnum;
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 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(AttrEnum.WIDTH.getAttr());
Object heightObj = map.get(AttrEnum.HEIGHT.getAttr());
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(AttrEnum.WIDTH.getAttr());
Object heightObj = map.get(AttrEnum.HEIGHT.getAttr());
Object pathObj = map.get("imagePath");
if (Objects.nonNull(widthObj) && Objects.nonNull(heightObj)) {
widthPx = (Double)map.get(AttrEnum.WIDTH.getAttr());
heightPx = (Double)map.get(AttrEnum.HEIGHT.getAttr());
} 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/" + UniqueUtil.getUuid() + "." + 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(AttrEnum.WIDTH.getAttr(), newWidthPx);
result.put(AttrEnum.HEIGHT.getAttr(), 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;
}
}
}