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();
        }
    }
}
posted @ 2022-07-01 17:43  Leejk  阅读(854)  评论(0编辑  收藏  举报