最近项目需要将知识库导出,导出为word
,网上查了很多资料,但是总会遇到很多磕磕绊绊,花了很多时间。希望对其他人有所借鉴。
利用word
来解析带base64
图片的富文本,思路是:word
模板另存为mht
格式,再修改后缀为ftl
。将base64
字符串的图片,填充到制作的模板上。
首先我们需要填充的word
模板如下:
首先利用MicroSoft Word
制作word
模板,由于尝试新建word
,后缀都是docx
的,但是我们要求的模板是doc
后缀的,因此在我们创建word
模板后需要另存为doc
格式(这点非常总要,坑了我好多天!!!!)。
我们的模板如下:
记住我们要替换的地方用 ${}
框起来,比如 $ {articleTitle}
和 $ {equipmentTypeName}
(去掉中间空格)。如上图所示。
将模板存为doc
格式后,再利用MicroSoft Word
另存为mht
格式,可以利用idea
进行打开。接下来就是对mht
格式进行修改了。
需要修改的地方:
- 全文搜索
gb2312
,然后将其替换为utf-8
,要记住同时需要加上3D
前缀。
需要改成一般如下格式(注意有的是单个双引号,有的是两个)
Content-Type: text/html; charset=3D"utf-8"
<meta http-equiv=3DContent-Type content=3D"text/html; charset=3Dutf-8">
- 同时在以下两个地方分别加入
$ {imagesBase64String}
和$ {imagesXmlHrefString}
,主要用于解析我们的base64
图片。
同时上图还有两个红色框框的地方,我们需要记住这些信息。后面有用。
修改后我们将mht
后缀改成ftl
。此外我们还要注意,格式转换后我们要替换的模板$ {articleTitle}
会填充一些其他字符,我们要将${}
内不是我们输入的字符删掉,最终变成以下:
还有就是,后面我们转换以后会发现我们的中文字符会出现很多乱码,比如我们指定的字体等线格式会变成未知的问好(乱码),还有我们基本信息
,文章名称
,设备类型名称
,故障类型
,故障代码
,文章内容
等中文字符都会变成乱码,我们可以手动输入中文进行修正。
带有?
显示的就是乱码。不过这些字体什么的不是这么重要,我们只需要把总要的我们模板的内容保证正确就行。
接下来是java
代码部分了。
下面handler
前三个set
的内容就是我们之前mht
用红色矩形框框起来的:“file:///C:/67194E70”
,“20201220.files”
,“01D6D6C1.A2FBEE20”
RichHtmlHandler handler = new RichHtmlHandler(String.valueOf(map.get("articleContent")));
handler.setDocSrcLocationPrex("file:///C:/67194E70");
handler.setDocSrcParent("20201220.files");
handler.setNextPartId("01D6D6C1.A2FBEE20");
handler.setShapeidPrex("_x56fe__x7247__x0020");
handler.setSpidPrex("_x0000_i");
handler.setTypeid("#_x0000_t75");
填充模板如下图所示,我们定义Map<String, Object>
,String
就是我们之前模板里面 ${articleTitle}
里面的字符,而Object
则分别表示我们要填充的内容。
Map<String, Object> map = new LinkedHashMap<>();
for (KDTO kDTO : kDTOList) {//遍历集合往map中添加数据
map.put("articleTitle", kDTO.getArticleTitle());
map.put("articleNumbe", kDTO.getArticleCode());
map.put("equipmentTypeName", kDTO.getEquipmentTypeName());
map.put("faultType", String.valueOf(kDTO.getFailureType()));
map.put("faultCode", kDTO.getArticleCode());
map.put("faultPhenomenon", kDTO.getFailureSymptom());
map.put("faultLocation", kDTO.getFailurePart());
map.put("creator", kDTO.getStaffName());
map.put("causeOfFailure", kDTO.getFailureReason());
map.put("treatmentMeasures", kDTO.getFailureTreatment());
map.put("articleContent", kDTO.getArticleContent());
我们通过freemarker
进行模板填充,下面是html
处理文章内容代码:
package com.fii.amms.utils;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.util.StringUtils;
import javax.imageio.ImageIO;
public class RichHtmlHandler {
private Document doc = null;
private String html;
private String docSrcParent = "";
private String docSrcLocationPrex = "";
private String nextPartId;
private String shapeidPrex;
private String spidPrex;
private String typeid;
private String handledDocBodyBlock;
private List<String> docBase64BlockResults = new ArrayList<String>();
private List<String> xmlImgRefs = new ArrayList<String>();
public String getDocSrcLocationPrex() {
return docSrcLocationPrex;
}
public void setDocSrcLocationPrex(String docSrcLocationPrex) {
this.docSrcLocationPrex = docSrcLocationPrex;
}
public String getNextPartId() {
return nextPartId;
}
public void setNextPartId(String nextPartId) {
this.nextPartId = nextPartId;
}
public String getHandledDocBodyBlock() {
String raw= WordHtmlGeneratorHelper.string2Ascii(doc.getElementsByTag("body").html());
return raw.replace("=3D", "=").replace("=", "=3D");
}
public String getRawHandledDocBodyBlock() {
String raw= doc.getElementsByTag("body").html();
return raw.replace("=3D", "=").replace("=", "=3D");
}
public List<String> getDocBase64BlockResults() {
return docBase64BlockResults;
}
public List<String> getXmlImgRefs() {
return xmlImgRefs;
}
public String getShapeidPrex() {
return shapeidPrex;
}
public void setShapeidPrex(String shapeidPrex) {
this.shapeidPrex = shapeidPrex;
}
public String getSpidPrex() {
return spidPrex;
}
public void setSpidPrex(String spidPrex) {
this.spidPrex = spidPrex;
}
public String getTypeid() {
return typeid;
}
public void setTypeid(String typeid) {
this.typeid = typeid;
}
public String getDocSrcParent() {
return docSrcParent;
}
public void setDocSrcParent(String docSrcParent) {
this.docSrcParent = docSrcParent;
}
public String getHtml() {
return html;
}
public void setHtml(String html) {
this.html = html;
}
public RichHtmlHandler(String html) {
this.html = html;
doc = Jsoup.parse(wrappHtml(this.html));
}
public void re_init(String html){
doc=null;
doc = Jsoup.parse(wrappHtml(html));
docBase64BlockResults.clear();
xmlImgRefs.clear();
}
public void handledHtml(boolean isWebApplication)
throws IOException {
Elements imags = doc.getElementsByTag("img");
if (imags == null || imags.size() == 0) {
// 返回编码后字符串
return;
//handledDocBodyBlock = WordHtmlGeneratorHelper.string2Ascii(html);
}
// 转换成word mht 能识别图片标签内容,去替换html中的图片标签
for (Element item : imags) {
// 把文件取出来
String src = item.attr("src");
String srcRealPath = src;
File imageFile = new File(srcRealPath);
String imageFielShortName = imageFile.getName();
//Base64的话获取fileTypeName;
String fileTypeName = srcRealPath.substring(srcRealPath.indexOf("/")+1, srcRealPath.indexOf(";"));
//String fileTypeName = WordImageConverter.getFileSuffix(srcRealPath);
String docFileName = "image" + UUID.randomUUID().toString() + "."+ fileTypeName;
String srcLocationShortName = docSrcParent + "/" + docFileName;
String styleAttr = item.attr("style"); // 样式
String encodedImg = null;
BufferedImage sourceImg = null;
String partSeparator = ",";
if (srcRealPath.contains(partSeparator)) {
encodedImg = srcRealPath.split(partSeparator)[1];
byte[] decodedImg = java.util.Base64.getDecoder().decode(encodedImg.getBytes(StandardCharsets.UTF_8));
sourceImg = ImageIO.read(new ByteArrayInputStream(decodedImg));
}
//高度
String imagHeightStr=item.attr("height");;
if(StringUtils.isEmpty(imagHeightStr)){
imagHeightStr = getStyleAttrValue(styleAttr, "height");
}
//宽度
String imagWidthStr=item.attr("width");;
if(StringUtils.isEmpty(imagHeightStr)){
imagHeightStr = getStyleAttrValue(styleAttr, "width");
}
imagHeightStr = imagHeightStr.replace("px", "");
imagWidthStr = imagWidthStr.replace("px", "");
if(StringUtils.isEmpty(imagHeightStr)){
//去得到默认的文件高度
imagHeightStr="0";
}
if(StringUtils.isEmpty(imagWidthStr)){
imagWidthStr="0";
}
//int imageHeight = Integer.parseInt(imagHeightStr);
//int imageWidth = Integer.parseInt(imagWidthStr);
int imageHeight = sourceImg.getHeight();
int imageWidth = sourceImg.getWidth();
// 得到文件的word mht的body块
String handledDocBodyBlock = WordImageConverter.toDocBodyBlock(srcRealPath,
imageFielShortName, imageHeight, imageWidth,styleAttr,
srcLocationShortName, shapeidPrex, spidPrex, typeid);
//这里的顺序有点问题:应该是替换item,而不是整个后面追加
//doc.rreplaceAll(item.toString(), handledDocBodyBlock);
item.after(handledDocBodyBlock);
// item.parent().append(handledDocBodyBlock);
item.remove();
// 去替换原生的html中的imag
//String base64Content = WordImageConverter.imageToBase64(srcRealPath);
String base64Content = encodedImg;
String contextLoacation = docSrcLocationPrex + "/" + docSrcParent + "/" + docFileName;
String docBase64BlockResult = WordImageConverter.generateImageBase64Block(nextPartId, contextLoacation,
fileTypeName, base64Content);
docBase64BlockResults.add(docBase64BlockResult);
String imagXMLHref = "<o:File HRef=3D\"" + docFileName + "\"/>";
xmlImgRefs.add(imagXMLHref);
}
}
private String getStyleAttrValue(String style, String attributeKey) {
if (StringUtils.isEmpty(style)) {
return "";
}
// 以";"分割
String[] styleAttrValues = style.split(";");
for (String item : styleAttrValues) {
// 在以 ":"分割
String[] keyValuePairs = item.split(":");
if (attributeKey.equals(keyValuePairs[0])) {
return keyValuePairs[1];
}
}
return "";
}
private String wrappHtml(String html){
// 因为传递过来都是不完整的doc
StringBuilder sb = new StringBuilder();
sb.append("<html>");
sb.append("<body>");
sb.append(html);
sb.append("</body>");
sb.append("</html>");
return sb.toString();
}
}
处理完后我们可以将文章内容和base64
图片分离:
RichHtmlHandler handler = new RichHtmlHandler(String.valueOf(map.get("articleContent")));
handler.setDocSrcLocationPrex("file:///C:/67194E70");
handler.setDocSrcParent("20201220.files");
handler.setNextPartId("01D6D6C1.A2FBEE20");
handler.setShapeidPrex("_x56fe__x7247__x0020");
handler.setSpidPrex("_x0000_i");
handler.setTypeid("#_x0000_t75");
try{
handler.handledHtml(false);
}catch (Exception e){
throw new AMMSException(ErrorEnum.FAILED_TO_EXPORT.getCode(), "导出知识库失败", ErrorEnum.FAILED_TO_EXPORT.getMsg());
}
String bodyBlock = handler.getHandledDocBodyBlock();
map.put("articleContent", bodyBlock);
String handledBase64Block = "";
if (handler.getDocBase64BlockResults() != null
&& handler.getDocBase64BlockResults().size() > 0) {
for (String item : handler.getDocBase64BlockResults()) {
handledBase64Block += item + "\n";
}
}
map.put("imagesBase64String", handledBase64Block);
String xmlimaHref = "";
if (handler.getXmlImgRefs() != null
&& handler.getXmlImgRefs().size() > 0) {
for (String item : handler.getXmlImgRefs()) {
xmlimaHref += item + "\n";
}
}
map.put("imagesXmlHrefString", xmlimaHref);
然后导出为ByteArrayOutputStream
,这里主要是需要导出多篇文章,然后将文章压缩。
public static ByteArrayOutputStream createWordBaos(Map<String, Object> dataMap, String templateName, ByteArrayOutputStream byteOut){
WordHtmlGeneratorHelper.handleAllObject(dataMap);
Writer w=null;
try {
Template t = configuration.getTemplate(templateName, "utf-8");
w = new OutputStreamWriter(byteOut);
t.process(dataMap, w);
} catch (Exception ex) {
ex.printStackTrace();
throw new RuntimeException(ex);
} finally {
if(w != null){
try {
w.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
用ByteArrayOutputStream
数组储存多篇文章的ByteArrayOutputStream
,用String
数组存储每个word
文件的名字,将多个文件流合并到压缩包下载:
public static void downLoadZip(ByteArrayOutputStream[] baoss, String[] fileNames, String zipName, HttpServletRequest request, HttpServletResponse response){
//压缩包输出流
ZipArchiveOutputStream zous = null;
try {
//压缩包响应输出流
zous = new ZipArchiveOutputStream(response.getOutputStream());
zous.setUseZip64(Zip64Mode.AsNeeded);
//response.setContentType("application/octet-stream");
//编码文件名并加上后缀(压缩包内部文件名不需要编码)
String filename = encodeChineseDownloadFileName(request, zipName) + ".zip";
response.setHeader("Content-disposition", "attachment; filename=" + filename);
for(int i = 0; i < baoss.length; i++){
ByteArrayOutputStream baos = baoss[i];
//给文件名
String fileName = fileNames[i];
//下面三行是吧excel的文件以流的形式转为byte[]
byte[] bytes = baos.toByteArray();
ArchiveEntry entry = new ZipArchiveEntry(fileName);
zous.putArchiveEntry(entry);
zous.write(bytes);
zous.closeArchiveEntry();
if (baos != null) {
baos.close();
}
}
} catch (IOException e) {
throw new ASException(FAILED_TO_EXPORT.getCode(), "导出失败", FAILED_TO_EXPORT.getMsg());
} catch (Exception e) {
throw new ASException(FAILED_TO_EXPORT.getCode(), "导出失败", FAILED_TO_EXPORT.getMsg());
} finally {
try {
if(zous != null){
zous.flush();
zous.close();
}
} catch (IOException e) {
throw new ASException(FAILED_TO_EXPORT.getCode(), "导出失败", FAILED_TO_EXPORT.getMsg());
}
}
}
try {
String fileName = URLEncoder.encode(new StringBuffer(knowledgeDTO.getArticleTitle())
.append(DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()))
.toString()
, ExcelData.UTF8) + ".doc";
fileNameArray[temp] = fileName;
Configuration cfg = new Configuration();
cfg.setDirectoryForTemplateLoading(new File(System.getProperty("user.dir") + "/src/main/resources/static"));
cfg.getTemplate("KnowledgeTemplate.ftl");
}catch (Exception e){
throw new AMMSException(ErrorEnum.FAILED_TO_EXPORT.getCode(), "导出知识库失败", ErrorEnum.FAILED_TO_EXPORT.getMsg());
}
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
WordGeneratorWithFreemarker.createWordBaos(map, "KnowledgeTemplate.ftl", byteOut);
byteOutArray[temp] = byteOut;
temp++;
}
String zipName = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now());
WordUtil.downLoadZip(byteOutArray, fileNameArray, zipName, request, response);
}
导出结果如下:
不过目前只能导出base64
图片,其他的格式还待进一步研究。