Word-Microsoft公式编辑器-OMML格式的公式转化为图片
思路:office中使用的Microsoft公式编辑器,例如word中公式为omml格式,转化为MathML格式,然后通过BufferedImage转化为图片格式。
踩坑较多,虽然以后大概率用不到了,记录一下便于他人使用。
工具类
使用封装好的工具类OmmlUtils;
public class OmmlUtils {
public static File convertOmathToPng(XmlObject xmlObject, ImageParse imageParser) {
Document document = null;
try {
String mathMLStr = getMathMLFromNode(xmlObject.getDomNode());
System.out.println("xxxxxxxxxxxxxxxxxxxxxxx : " + mathMLStr);
document = W3cNodeUtil.xmlStr2Node(mathMLStr, "utf-16");
String imageName = documentToImageHTML(document, imageParser);
if (imageName != null) {
File tf = new File(imageName);
System.out.println("图片是否存在 ======== " + tf.exists() + "Name : " + tf.getName());
return tf;
}
} catch (IOException | TransformerException e) {
e.printStackTrace();
}
return null;
}
/**
* 直接转node有等号会出问题,先转成xml的string,再转成mathML的字符串
*
* @param node
* @return
* @throws IOException
* @throws TransformerException
*/
private static String getMathMLFromNode(Node node) throws IOException, TransformerException {
final File xslFile = new File("/omml2mml.xsl");
// final File xslFile = new File("D:\\MyCode\\omml-test\\omml2mml.xsl");
// boolean exists = xslFile.exists();
StreamSource streamSource = new StreamSource(xslFile);
String s = W3cNodeUtil.node2XmlStr(node);
// encoding utf-16
String mathML = W3cNodeUtil.xml2Xml(s, streamSource);
if (mathML != null) {
mathML = mathML.replaceAll("xmlns:m=\"http://schemas.openxmlformats.org/officeDocument/2006/math\"", "");
mathML = mathML.replaceAll("xmlns:mml", "xmlns");
mathML = mathML.replaceAll("mml:", "");
}
return mathML;
}
private static String documentToImageHTML(Document document, ImageParse imageParser) {
try {
Converter mathMLConvert = Converter.getInstance();
LayoutContextImpl localLayoutContextImpl = new LayoutContextImpl(LayoutContextImpl.getDefaultLayoutContext());
localLayoutContextImpl.setParameter(Parameter.MATHSIZE, 50);
ByteArrayOutputStream os = new ByteArrayOutputStream();
mathMLConvert.convert(document, os, "image/png", localLayoutContextImpl);
String pngName = imageParser.parse(os.toByteArray(), ".png");
os.close();
return pngName;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
W3cNodeUtil:
/**
* description: W3cNodeUtil
* author: JKL
* date: 2022/6/27 21:55
*/
public class W3cNodeUtil {
public static String node2XmlStr(Node node) {
Transformer transformer = null;
if (node == null) {
throw new IllegalArgumentException("node 不能为空..");
}
try {
transformer = TransformerFactory.newInstance().newTransformer();
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
if (transformer != null) {
try {
StringWriter sw = new StringWriter();
transformer.transform(new DOMSource(node), new StreamResult(sw));
return sw.toString();
} catch (TransformerException te) {
throw new RuntimeException(te.getMessage());
}
}
return null;
}
public static String xml2Xml(String xml, Source XSLSource) {
Transformer transformer = null;
if (xml == null) {
throw new IllegalArgumentException("node 不能为空..");
}
try {
if (XSLSource == null) {
transformer = TransformerFactory.newInstance().newTransformer();
} else {
transformer = TransformerFactory.newInstance().newTransformer(XSLSource);
}
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
if (transformer != null) {
try {
Source source = new StreamSource(new StringReader(xml));
StringWriter sw = new StringWriter();
transformer.transform(source, new StreamResult(sw));
return sw.toString();
} catch (TransformerException te) {
throw new RuntimeException(te.getMessage());
}
}
return null;
}
//MathML转成Document
public static Document xmlStr2Node(String xmlString, String encoding) {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
Document doc = null;
try {
InputStream is = new ByteArrayInputStream(xmlString.getBytes(encoding));
doc = dbf.newDocumentBuilder().parse(is);
is.close();
} catch (Exception e) {
e.printStackTrace();
}
return doc;
}
private static Node getChildNode(Node node, String nodeName) {
if (!node.hasChildNodes()) {
return null;
}
NodeList childNodes = node.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node childNode = childNodes.item(i);
if (nodeName.equals(childNode.getNodeName())) {
return childNode;
}
childNode = getChildNode(childNode, nodeName);
if (childNode != null) {
return childNode;
}
}
return null;
}
public static Node getChildChainNode(Node node, String... nodeName) {
Node childNode = node;
for (int i = 0; i < nodeName.length; i++) {
String tmp = nodeName[i];
childNode = getChildNode(childNode, tmp);
if (childNode == null) {
return null;
}
}
return childNode;
}
}
ImageParse:持久化到硬盘的一个类,可选择使用
/**
* description: ImageParse
* author: JKL
* date: 2022/6/27 22:03
*/
public class ImageParse {
private int number = 0;
// private String targetDir;
public ImageParse() {
super();
// this.targetDir = targetDir;
}
public String parse(byte[] data, String extName) {
return parse(new ByteArrayInputStream(data), extName);
}
public String parse(InputStream in, String extName) {
if (extName.lastIndexOf(".") > -1) {
extName = extName.substring(extName.lastIndexOf(".") + 1);
}
String filename = "image_" + (number++) + "." + extName;
// File target = new File(targetDir);
// if (!target.exists()) {
// target.mkdirs();
// }
try {
IOUtils.copy(in, new FileOutputStream(filename));
} catch (IOException e) {
e.printStackTrace();
}
return filename;
}
}
使用
File file = OmmlUtils.convertOmathToPng(object, imageParser);
pom文件
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.17</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.17</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>3.17</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>net.arnx</groupId>
<artifactId>wmf2svg</artifactId>
<version>0.9.8</version>
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-codec</artifactId>
<version>1.7</version>
</dependency>
<dependency>
<groupId>net.sourceforge.jeuclid</groupId>
<artifactId>jeuclid-core</artifactId>
<version>3.1.9</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
踩坑
首先我是提供接口给他人调用:如下:
@PostMapping("/single")
static DEV_TOKEN;
static BUCKET_NAME;
static OBJECT_NAME;
public FileDTO test2(@RequestBody XMLBody xmlBody) throws XmlException {
XmlObject object = XmlObject.Factory.parse(xmlBody.getXmlBody());
ImageParse imageParser = new ImageParse();
File file = OmmlUtils.convertOmathToPng(object, imageParser);
FileDTO fileDTO = null;
if (file != null) {
// System.out.println("File是否存在================="+ file.exists());
Map<String, String> map = new HashMap<String, String>();
map.put("bucketName", BUCKET_NAME);
map.put("devToken", DEV_TOKEN);
map.put("objectName", OBJECT_NAME + "/" + file.getName());
map.put("fileDownloadName", "");
String result = HttpUtils.doPostUploadFile("https://", map, file);
log.info("调用httpclient返回结果:" + result);
fileDTO = new FileDTO();
fileDTO.setMessage("操作成功");
fileDTO.setOssReturn(result);
// file.delete();
return fileDTO;
} else {
fileDTO = new FileDTO();
fileDTO.setMessage("操作失败");
// file.delete();
return fileDTO;
}
}
使用公司上传到阿里云oss的通用接口,先调用: HttpUtils.doPostUploadFile("https://", map, file);
map是公司规定的请求体参数,file是文件;
使用HttpUtis工具类,以multipart/form-data方式
/**
* HttpUtils
*
* @author Rot
* @date 2021/10/15 17:45
*/
@Slf4j
public class HttpUtils {
/**
* 从连接池中获取连接的超时时间--10s
*/
private static int connectionRequestTimeout = 30000;
/**
* 客户端和服务器建立连接的超时时间--握手连接时间--10s
*/
private static int connectTimeout = 60000;
/**
* 从对方服务接受响应流的时间
*/
private static int socketTimeout = 60000;
/**
* 连接池最大连接数
*/
private static int maxTotal = 800;
/**
* 每个主机的并发
*/
private static int maxPerRoute = 20;
private static PoolingHttpClientConnectionManager connectionManager = null;
private static CloseableHttpClient httpClient;
public static CloseableHttpClient getClient() {
return httpClient;
}
static {
log.info("初始化http connection 连接池 ...");
try {
// 配置同时支持 HTTP 和 HTPPS
SSLContextBuilder builder = new SSLContextBuilder();
builder.loadTrustMaterial(null, new TrustSelfSignedStrategy());
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(builder.build());
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create().register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", sslConnectionSocketFactory).build();
connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
} catch (Exception e) {
log.error("初始化http 连接池异常", e);
connectionManager = new PoolingHttpClientConnectionManager();
}
//连接池统一配置
connectionManager.setMaxTotal(maxTotal);
connectionManager.setDefaultMaxPerRoute(maxPerRoute);
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(connectTimeout).setConnectionRequestTimeout(connectionRequestTimeout).setSocketTimeout(socketTimeout).build();
//不做重试功能
HttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler(0, false);
httpClient = HttpClients.custom().setConnectionManager(connectionManager).setDefaultRequestConfig(requestConfig).setRetryHandler(retryHandler).build();
ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
scheduledExecutorService.scheduleWithFixedDelay(() -> {
connectionManager.closeExpiredConnections();
connectionManager.closeIdleConnections(60, TimeUnit.SECONDS);
log.info("回收过期的http连接完成 status:{}", connectionManager.getTotalStats());
}, 60, 120, TimeUnit.SECONDS);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
log.info("关闭 httpClient 连接");
try {
if (httpClient != null) {
httpClient.close();
}
} catch (IOException e) {
log.error("关闭 httpClient 异常", e);
}
}));
}
/**
* post请求提交form-data上传文件
*
* @param url
* @param headers 请求头
* @return
*/
public static String doPostUploadFile(String url, Map<String, String> headers, File file) {
HttpPost httpPost = new HttpPost(url);
// packageHeader(headers, httpPost);
String fileName = file.getName();
CloseableHttpResponse response = null;
String respContent = null;
long startTime = System.currentTimeMillis();
// 设置请求头 boundary边界不可重复,重复会导致提交失败
String boundary = "-------------------------" + UUID.randomUUID().toString();
httpPost.setHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
// 创建MultipartEntityBuilder
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
// 设置字符编码
builder.setCharset(StandardCharsets.UTF_8);
// 模拟浏览器
builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
// 设置边界
builder.setBoundary(boundary);
// 设置multipart/form-data流文件
builder.addPart("multipartFile", new FileBody(file));
// application/octet-stream代表不知道是什么格式的文件
builder.addBinaryBody("file", file, ContentType.create("application/octet-stream"), fileName);
if (!CollectionUtils.isEmpty(headers)) {
Set<Map.Entry<String, String>> entrySet = headers.entrySet();
for (Map.Entry<String, String> entry : entrySet) {
System.out.println(entry.getKey());
builder.addTextBody(entry.getKey(), entry.getValue());
}
}
HttpEntity entity = builder.build();
httpPost.setEntity(entity);
try {
response = httpClient.execute(httpPost);
if (response != null && response.getStatusLine() != null && response.getStatusLine().getStatusCode() < 400) {
log.info("http状态码 :" + response.getStatusLine().getStatusCode());
HttpEntity he = response.getEntity();
if (he != null) {
respContent = EntityUtils.toString(he, "UTF-8");
}
} else {
log.error("对方响应的状态码不在符合的范围内!");
throw new RuntimeException();
}
return respContent;
} catch (Exception e) {
log.error("网络访问异常,请求url地址={},响应体={},error={}", url, response, e);
throw new RuntimeException();
} finally {
log.info("统一外网请求参数打印,post请求url地址={},响应={},耗时={}毫秒", url, respContent, (System.currentTimeMillis() - startTime));
try {
if (response != null) {
response.close();
}
} catch (IOException e) {
log.error("请求链接释放异常", e);
}
}
}
/**
* 封装请求头
*
* @param paramsHeads
* @param httpMethod
*/
private static void packageHeader(Map<String, String> paramsHeads, HttpRequestBase httpMethod) {
if (!CollectionUtils.isEmpty(paramsHeads)) {
Set<Map.Entry<String, String>> entrySet = paramsHeads.entrySet();
for (Map.Entry<String, String> entry : entrySet) {
System.out.println(entry.getKey());
httpMethod.setHeader(entry.getKey(), entry.getValue());
}
}
}
}
解析xml格式的omml时需要使用到 omml2mml.xsl 文件,网上可自行下载
坑点1:
你会发现本地调用没有问题,扔到线上会出问题;报错:
全局异常信息:Handler dispatch failed; nested exception is java.lang.UnsatisfiedLinkError: /usr/local/openjdk-8/jre/lib/amd64/libfontmanager.so: libfreetype.so.6: cannot open shared object file: No such file or directory
此时将docker的jdk源改由 openjdk 改为frolvlad/alpine-java:jdk8-slim,可解析成功
坑点2:
数字,符号等都有能解析出来了生成图片了,但是中文字符都成了乱码。
需要在jdk目录下的 /usr/lib/jvm/java-8-oracle/jre/lib/fonts放入windows下的简体宋体文件:C:\Windows\Fonts\simsun
写个单元测试调用,发现本地和线上都可以使用了!
public class xmlTest {
@Test
public void xxx() {
XMLBody xmlBody = new XMLBody();
xmlBody.setXmlBody("<m:oMath xmlns=\"http://schemas.openxmlformats.org/officeDocument/2006/math\" xmlns:m=\"http://schemas.openxmlformats.org/officeDocument/2006/math\" xmlns:s=\"http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes\" xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\" xmlns:xml=\"http://www.w3.org/XML/1998/namespace\"><m:r><m:rPr></m:rPr><m:t>=</m:t></m:r><m:f><m:fPr><m:ctrlPr></m:ctrlPr></m:fPr><m:num><m:r><m:rPr></m:rPr><m:t>部分</m:t></m:r><m:ctrlPr></m:ctrlPr></m:num><m:den><m:r><m:rPr></m:rPr><m:t>整体</m:t></m:r><m:ctrlPr></m:ctrlPr></m:den></m:f></m:oMath>");
String json = JSONObject.toJSONString(xmlBody);
doPost(json);
}
private void doPost(String body) {
String methodUrl = "";
// String methodUrl = "http://127.0.0.1:8086/omml/v1/convert/single";
HttpURLConnection connection = null;
OutputStream dataout = null;
BufferedReader reader = null;
String line = null;
try {
URL url = new URL(methodUrl);
connection = (HttpURLConnection) url.openConnection();// 根据URL生成HttpURLConnection
connection.setDoOutput(true);// 设置是否向connection输出,因为这个是post请求,参数要放在http正文内,因此需要设为true,默认情况下是false
connection.setDoInput(true); // 设置是否从connection读入,默认情况下是true;
connection.setRequestMethod("POST");// 设置请求方式为post,默认GET请求
connection.setUseCaches(false);// post请求不能使用缓存设为false
connection.setConnectTimeout(3000);// 连接主机的超时时间
connection.setReadTimeout(3000);// 从主机读取数据的超时时间
connection.setInstanceFollowRedirects(true);// 设置该HttpURLConnection实例是否自动执行重定向
connection.setRequestProperty("connection", "Keep-Alive");// 连接复用
connection.setRequestProperty("charset", "utf-8");
connection.setRequestProperty("Content-Type", "application/json");
connection.connect();// 建立TCP连接,getOutputStream会隐含的进行connect,所以此处可以不要
dataout = new DataOutputStream(connection.getOutputStream());// 创建输入输出流,用于往连接里面输出携带的参数
System.out.println(body);
dataout.write(body.getBytes());
dataout.flush();
dataout.close();
reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));// 发送http请求
StringBuilder result = new StringBuilder();
// 循环读取流
while ((line = reader.readLine()) != null) {
result.append(line).append(System.getProperty("line.separator"));//
}
System.out.println(result.toString());
} catch (
IOException e) {
e.printStackTrace();
} finally {
try {
if (reader != null)
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
if (connection != null)
connection.disconnect();
}
}
}
你应当热爱自由!
作者:Leejk,转载请注明原文链接:https://www.cnblogs.com/leejk/p/16435495.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)