Java 富文本 导出 word 压缩包

  • 1、场景
  • 2、切入点
  • 3、开发
  • 1)主要思路
  • 2)实现
  • 1、mht 模板
  • 2、占位符替换
  • 3、完整 demo
  • 4、常见问题
  • 1)mht 替换出错
  • 2)文件名重名

1、场景

文章详情、产品详情等带图和样式的文案数据,在 db 里边存了富文本,需要导出到 word 压缩包。

2、切入点

Q:图片数据要么是网络图片、要么是本地图片,要怎么转到 word 显示呢?
A:base64

Q:样式不是固定模板,怎么绘制成 word 呢?
A:曲线救国,制作 mht 模板,把数据填充到模板里边,再转 word 吐出。

3、开发

1)主要思路

1、制作 mht 文件,观察内容格式,修改加参,读取 mht 模板。
2、从 db 取出富文本内容,分割标签,分别模仿 mht 的编码规律动态拼接 html 非图片数据、图片 base64 字符串、图片的引用位置,然后替换 mht 里边的占位符,得到 mht 字符串。
3、 mht 字符串转换为 word 并打包。

2)实现

1、mht 模板

1)如果懒得自己拼接,可以直接下载博主的 newsContent.mht。
2)模板制作。用 office 新建一个 doc,写几个文字,和插入一张图片,另存为 mht 尾缀的文件。mht 文件在编码的时候大概的规律:文档的最末尾有一个 xml,里面规定了各种资源的引用位置,在这个 xml 上面就是各种资源的 base64 编码,而实际显示的 body 部分只是做了一个引用而已。
用记事本打开 mht,搜索 image00,分别找到图片的资源引用位置、 base64 部分、body 对图片的引用。


图1 资源引用位置

java html转换手机端图片 java html转word 图片_字符串


图2 base64 部分

java html转换手机端图片 java html转word 图片_字符串_02


图3 body 对图片的引用

java html转换手机端图片 java html转word 图片_java_03

2、占位符替换

分别模仿以上三部分动态拼接图片序号及标签内容,用于等下替换占位符。

//图片的资源引用位置
contentImageRefString.append("\n" + "\n" + "<o:File HRef=3D\"image" + i + j + ".png\"/>");
//base64 部分
contentImageString.append("\n" +
        "\n" +
        "------=_NextPart_01D71F2C.734F7C10\n" +
        "Content-Location: file:///C:/D125FE07/file8007.files/image" +
        i +
        j +
        ".png\n" +
        "Content-Transfer-Encoding: base64\n" +
        "Content-Type: image/png\n" +
        "\n" +
        imgStr);
//body 对图片的引用
contentString.append("<v:shape id=3D\"图片_x0020_1\" o:spid=3D\"_x0000_i1025\" type=3D\"=\n" +
        "#_x0000_t75\"\n" +
        " style=3D'width:" + imgWidth + "pt;height:" + imgHeight + "pt;visibility:visible;mso-wrap-style:squ=\n" +
        "are'>\n" +
        " <v:imagedata src=3D\"file8007.files/image" +
        i +
        j +
        ".png\" o:title=3D\"\"/>\n" +
        "</v:shape>");

修改 mht 中图1、图2、图3,加上占位符

java html转换手机端图片 java html转word 图片_字符串_04


java html转换手机端图片 java html转word 图片_3D_05

java html转换手机端图片 java html转word 图片_java html转换手机端图片_06

3、完整 demo

package exportWordFromUEditor;

import fai.comm.util.FaiList;
import sun.misc.BASE64Encoder;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * @author Aunty Cai
 */
public class Export {

    /**
     * 读取 mht 文件到字符串
     * @param filePath
     * @return
     * @throws Exception
     */
    public static String readFile(String filePath) throws Exception {
        StringBuffer buffer = new StringBuffer("");
        BufferedReader br = null;
        try {
            br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "UTF-8"));
            buffer = new StringBuffer();
            while (br.ready()){
                buffer.append((char) br.read());
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (br != null){
                br.close();
            }
        }
        return buffer.toString();
    }

    /**
     * 将网络图片编码为base64
     *
     * @param url
     * @return
     */
    public static String encodeImageToBase64(URL url) {
        //将图片文件转化为字节数组字符串,并对其进行Base64编码处理
        HttpURLConnection conn = null;
        try {
            conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(5 * 1000);
            InputStream inStream = conn.getInputStream();
            ByteArrayOutputStream outStream = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = inStream.read(buffer)) != -1) {
                outStream.write(buffer, 0, len);
            }
            inStream.close();
            byte[] data = outStream.toByteArray();
            //对字节数组Base64编码
            BASE64Encoder encoder = new BASE64Encoder();
            String base64 = encoder.encode(data);
            return base64;
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("将网络图片编码为base64 异常!e=" + e);
            return "";
        }
    }

    /**
     * 截取富文本编辑器中完整的<img/>标签,并调用方法将其替换为可读取的base64字符串填回富文本编辑器中
     * @param editorContent
     * @param template
     * @return
     * @throws Exception
     */
    public static String addEditorImage2Template(String editorContent, String template) throws Exception {
        StringBuffer contentString = new StringBuffer(); //存储template主内容
        StringBuffer contentImageString = new StringBuffer(); //存储template图片的base64字符串
        StringBuffer contentImageRefString = new StringBuffer(); //存储template图片的引用位置
        if (editorContent != null){
            String[] stringSplit_img = editorContent.split("<img"); //分割<img/>标签
            for (int i=0; i<stringSplit_img.length; i++){
                if (i != 0){
                    String[] stringSplit_p = ("<img" + stringSplit_img[i]).split("</p>"); //分割<p/>标签
                    for (int j=0; j<stringSplit_p.length; j++){
                        if (j != 0){
                            contentString.append("</p>" + stringSplit_p[j]);
                        }else {
                            //此时stringSplit_p[j]已为完整的<img.../>标签字符串
                            //截取图片资源路径
                            for (String s : stringSplit_p[j].split(" ")){
                                if (s.startsWith("src=")){
                                    s = s.replace("src=\"", "");
                                    s = s.replace("\"", "");
                                    URL imgUrl = new URL(s);
                                    String imgStr = encodeImageToBase64(imgUrl);
                                    //base64 部分
                                    contentImageString.append("\n" +
                                            "\n" +
                                            "------=_NextPart_01D71F2C.734F7C10\n" +
                                            "Content-Location: file:///C:/D125FE07/file8007.files/image" +
                                            i +
                                            j +
                                            ".png\n" +
                                            "Content-Transfer-Encoding: base64\n" +
                                            "Content-Type: image/png\n" +
                                            "\n" +
                                            imgStr);
                                }else if (s.startsWith("style")){
                                    String regex_width = "width:(?<width>\\d+([.]\\d+)?)px;";
                                    String regex_height = "height:(?<height>\\d+([.]\\d+)?)px;";
                                    double imgWidth = 0;
                                    double imgHeight = 0;
                                    Pattern pattern = Pattern.compile(regex_width);
                                    Matcher matcher = pattern.matcher(s);
                                    if (matcher.find()){
                                        imgWidth = Double.valueOf(matcher.group("width"));
                                    }
                                    matcher = Pattern.compile(regex_height).matcher(s);
                                    if (matcher.find()){
                                        imgHeight = Double.valueOf(matcher.group("height"));
                                    }
                                    //pt=px乘以3/4
                                    imgWidth = imgWidth*3/4;
                                    imgHeight = imgHeight*3/4;
                                    //body 对图片的引用
                                    contentString.append("<v:shape id=3D\"图片_x0020_1\" o:spid=3D\"_x0000_i1025\" type=3D\"=\n" +
                                            "#_x0000_t75\"\n" +
                                            " style=3D'width:" + imgWidth + "pt;height:" + imgHeight + "pt;visibility:visible;mso-wrap-style:squ=\n" +
                                            "are'>\n" +
                                            " <v:imagedata src=3D\"file8007.files/image" +
                                            i +
                                            j +
                                            ".png\" o:title=3D\"\"/>\n" +
                                            "</v:shape>");
                                    //图片的资源引用位置
                                    contentImageRefString.append("\n" + "\n" +
                                            "<o:File HRef=3D\"image" + i + j + ".png\"/>");
                                }else if (s.contains("</a>")){
                                    contentString.append("</a>");
                                }
                            }
                        }
                    }
                } else {
                    contentString.append(stringSplit_img[i]);
                }
            }
        }
        template = template.replace("${CONTENT}", contentString.toString());
        template = template.replace("${CONTENTIMAGE}", contentImageString.toString());
        template = template.replace("${CONTENTREF}", contentImageRefString.toString());
        return template;
    }

    public static void main(String[] args) throws Exception {
        //1.读取mht文件获得mht字符串(需传入文件路径);
        String template = readFile("D:\\workspace\\test\\src\\exportWordFromUEditor\\newsContent.mht");

        //2.从 db 取出富文本编辑器的字符串,调用富文本编辑器的处理方法,需传参(富文本编辑器字符串,mht模版字符串),返回处理之后的mht字符串;
        String editorContent = "<div>\n" +
                "    <p style=\"line-height:1.5em;\">\n" +
                "        测试下载doc测试下载doc测试下载doc\n" +
                "    </p>\n" +
                "    <p style=\"line-height:1.5em;\">\n" +
                "        一个图片\n" +
                "    </p>\n" +
                "    <p style=\"line-height:1.5em;\">\n" +
                "        <img src=\"https://codechina.csdn.net/cjw0001/imgfortest/-/raw/4fef1bad8502a080da7f243fbed5eaa3f0ba7e87/kuangfen.png\" style=\"width:577px;height:592px;\" />\n" +
                "    </p>\n" +
                "</div>";

        //3.从 db 取出对应图片,调用图片转base64字符串的方法,然后将得到的base64字符串去replace mht字符串中图片的占位符,如(${image});
        String result = addEditorImage2Template(editorContent, template);

        /************************************************导出word begin************************************************/
        FileOutputStream fos = null;
        try {
            byte b[] = result.getBytes("gb2312");
            fos = new FileOutputStream("导出富文本到word.docx");
            fos.write(b);
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fos != null){
                fos.close();
            }
        }
        /************************************************导出word end*************************************************/

        /************************************************导出zip begin************************************************/
        FaiList<String> contentList = new FaiList<String>();
        contentList.add(result);
        contentList.add(result);
        ZipOutputStream zos = null;
        try {
            zos = new ZipOutputStream(new FileOutputStream("./导出富文本到word.zip"));
            int i=0;
            for (String content : contentList){
                i++;
                String name = "测试" + i;
                ZipEntry zipEntry = new ZipEntry(name + ".docx");
                zos.putNextEntry(zipEntry);
                byte[] bytes = content.getBytes("gb2312");
                zos.write(bytes, 0, bytes.length);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (zos != null){
                zos.close();
            }
        }
        /************************************************导出zip end*************************************************/
    }
}

4、常见问题

1)mht 替换出错

base64 字符串和 xml 中替换的内容,文件名必须对应。而且base64字符串会有一个引用头,里面的部分代码必须另起一行顶头写。否则会失败。博主已经在完整 demo 里边加上对应的 \n 了。

2)文件名重名

对于导出 zip 的需求,如果 db 里边本来就允许文件名重名,生成 doc 塞进 zip 时就会异常:
java.util.zip.ZipException: duplicate entry
可以参考这篇 【Java】导出的压缩文件名重复则在后面叠加(数字)

ps:查阅了超多奇奇怪怪的参考文档,都是不太完整的,于是博主生气地撸了这么个工具。亲测可用。