文章目录
- 前言
- 一、freemarker实现内容替换
- 二、ftl 模板
- 1.word另存ftl
- 2.编辑ftl文件
- 2.1 了解一下常用的标记及其说明
- 2.2 list处理
- 2.3 红线
- 2.4 图片
- 总结
前言
固定内容word生成:freemarker ftl模板
动态表格生成:https://blog.csdn.net/mr_wanter/article/details/126763195
一、freemarker实现内容替换
maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
模板内容替换工具类
import com.spire.doc.Document;
import com.spire.doc.FileFormat;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.Version;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.util.Map;
import java.util.UUID;
@Component
public class DocUtils implements InitializingBean {
/**
* 设置使用的编码格式
*/
private static final String CHARSET = "UTF-8";
/**
* 设置使用的版本
*/
private static final String VERSION = "2.3.0";
//加载Word模板的路径
@Value("${analysis.report.template.path}")
private String analysisReportTemplatePath;
//输出Word路径
@Value("${analysis.report.res.path}")
private String analysisReportResPath;
private static String wordModePath;
private static String generatePath;
@Override
public void afterPropertiesSet() {
DocUtils.wordModePath = analysisReportTemplatePath;
DocUtils.generatePath = analysisReportResPath;
}
public static String dutyDownloadReport(Map<String, Object> wordData, String wordModeFile, String fileName) {
try {
// 2. 设置配置内容
// 设置版本
Configuration configuration = new Configuration(new Version(VERSION));
// 指定加载Word模板的路径
configuration.setDirectoryForTemplateLoading(new File(wordModePath));
// 以UTF-8的编码格式,读取模板文档
Template template = configuration.getTemplate(wordModeFile, CHARSET);
// 3. 输出文档路径及名称
File outFile = new File(generatePath + fileName + UUID.randomUUID() + ".doc");
Writer writer = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(outFile.toPath()), CHARSET), 10240);
// 输出
template.process(wordData, writer);
writer.flush();
writer.close();
return outFile.getAbsolutePath();
} catch (TemplateException | IOException e) {
e.printStackTrace();
}
return null;
}
public static void downloadFile(HttpServletResponse response, Map<String, Object> dataMap, String template, String fileName) {
OutputStream out = null;
FileInputStream fileInputStream = null;
String filePath="";
try {
filePath = DocUtils.dutyDownloadReport(dataMap, template, fileName);
String exportName = fileName+System.currentTimeMillis();
exportName = URLEncoder.encode(exportName, "utf-8");
File file;
//预览兼容doc转换docx
if (filePath.endsWith("doc")){
Document doc = new Document();
doc.loadFromFile(filePath);
String docName = filePath.substring(0,filePath.lastIndexOf("."));
doc.saveToFile(docName+".docx", FileFormat.Docx);
file = new File(docName+".docx");
}else {
file = new File(filePath);
}
fileInputStream = new FileInputStream(file);
response.setContentType("application/octet-stream");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-Disposition", "attachment; filename=" + exportName + ".docx");
response.setHeader("FileName", exportName + ".docx");
response.setHeader("Access-Control-Expose-Headers", "FileName");
//5. 创建缓冲区
int len = 0;
byte[] bytes = new byte[1024];
//6. 获取OutputStream()对象
out = response.getOutputStream();
//7. 将fileOutputStream流写入到buffer缓冲区,并使用OutputStream将缓冲区中的数据输入到客户端
while ((len = fileInputStream.read(bytes))>0){
out.write(bytes,0,len);
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
if(fileInputStream !=null){
fileInputStream.close();
}
if(out !=null){
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
调用示例
public FileResp wordCreate(String eventId,String type, HttpServletResponse response, HttpServletRequest request) {
//构造替换内容的map
HashMap<String, Object> dataMap = new HashMap<>();
String nowDateStr = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), DatePattern.CHINESE_DATE_PATTERN);
dataMap.put("nowDate", nowDateStr);
String filePath = "";
try {
filePath = DocUtils.dutyDownloadReport(dataMap, "decision_plan.ftl", "决策方案模板");
File file;
//预览兼容doc转换docx
if (filePath.endsWith("doc")) {
Document doc = new Document();
doc.loadFromFile(filePath);
String docName = filePath.substring(0, filePath.lastIndexOf("."));
doc.saveToFile(docName + ".docx", FileFormat.Docx);
file = new File(docName + ".docx");
} else {
file = new File(filePath);
}
// 附件表保存
FileResp fileResp = fileService.upload(file, "decision");
// 决策方案业务表保存
save(DecisionPlanReq.builder().eventId(eventId).fileId(fileResp.getFileId()).reportName(reportName + DateUtil.format(LocalDateTime.now(), "yyyyMMddHHmmss")).build());
return fileResp;
} catch (Exception e) {
e.printStackTrace();
throw new BusinessException("方案生成失败");
}
}
生成效果
二、ftl 模板
1.word另存ftl
word另存为xml后更改名称为decision_plan.ftl
2.编辑ftl文件
编辑必要性:
- 上述步骤生成的ftl会出现一段内容被分割成多个
<w:p></w:p>
,但是对于替换文本也会出现类似情况导致替换时发生找不到并报错的情况${eventTitle}
,这时需手动更改。 - 针对list数据多行展示或表格需手动处理。
- 示例中的红线
- 图片插入
2.1 了解一下常用的标记及其说明
- <w:wordDocument>: Word文档的根元素,表示整个文档。
- <w:body>: 文档的主体部分,包含了段落和其他内容。
- <w:p>: 表示一个段落,即文档中的一个文本块或一个文本行。
- <w:r>: 表示一个运行(Run),即段落中的一个文本范围,可以具有不同的格式和样式。
- <w:t>: 表示文本内容,位于<w:r>标签内部,用于包含实际的文本字符串。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<?mso-application progid="Word.Document"?>
<w:wordDocument xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:sl="http://schemas.microsoft.com/schemaLibrary/2003/core" xmlns:aml="http://schemas.microsoft.com/aml/2001/core" xmlns:wx="http://schemas.microsoft.com/office/word/2003/auxHint" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" w:macrosPresent="no" w:embeddedObjPresent="no" w:ocxPresent="no" xml:space="preserve" xmlns:wpsCustomData="http://www.wps.cn/officeDocument/2013/wpsCustomData">
<w:body>
<w:p>
<!--段落样式写这里-->
<w:pPr>
<!--居中对齐-->
<w:jc w:val="center"/>
</w:pPr>
<w:r>
<!--内容样式写这里-->
<w:rPr>
<w:rFonts w:ascii="微软雅黑" w:h-ansi="微软雅黑" w:fareast="微软雅黑" w:cs="微软雅黑" w:hint="fareast"/>
<!--粗体-->
<w:b/>
<w:b-cs/>
<!--字体大小-->
<w:sz w:val="36"/>
<w:sz-cs w:val="44"/>
</w:rPr>
<w:t>指挥决策方案</w:t>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:jc w:val="center"/>
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="微软雅黑" w:h-ansi="微软雅黑" w:fareast="微软雅黑" w:cs="微软雅黑" w:hint="fareast"/>
<w:sz w:val="24"/>
<w:sz-cs w:val="32"/>
</w:rPr>
<w:t>第x期</w:t>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:jc w:val="center"/>
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="微软雅黑" w:h-ansi="微软雅黑" w:fareast="微软雅黑" w:cs="微软雅黑" w:hint="default"/>
<w:b/>
<w:b-cs/>
<w:sz w:val="28"/>
<w:sz-cs w:val="36"/>
</w:rPr>
<w:t>${eventTitle}</w:t>
</w:r>
</w:p>
</w:body>
</w:wordDocument>
2.2 list处理
<#list approveList as row>
<w:p>
<w:r>
<w:rPr>
<w:rFonts w:ascii="宋体" w:h-ansi="宋体" w:fareast="宋体" w:cs="宋体" w:hint="default"/>
<w:color w:val="333333"/>
<w:kern w:val="0"/>
<w:sz w:val="24"/>
</w:rPr>
<w:t>${row.id}</w:t>
</w:r>
</w:p>
</#list>
2.3 红线
2.4 图片
- html格式的直接img标签读取
- Office Open XML文档需要将图片转为base64渲染
<#list process.fileRespList as file>
<w:p>
<w:pPr>
<w:rPr>
<w:rFonts w:fareast="宋体" w:hint="fareast"/>
<w:lang w:fareast="ZH-CN"/>
</w:rPr>
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:fareast="宋体" w:hint="fareast"/>
<w:lang w:fareast="ZH-CN"/>
</w:rPr>
<w:pict>
<w:binData w:name="wordml://1.png">${file.fileBase64} </w:binData>
<v:shape id="_x0000_s1026" o:spt="75" alt="Snipaste_2024-05-10_17-26-29" type="#_x0000_t75" style="height:113.25pt;width:188.25pt;" filled="f" o:preferrelative="t" stroked="f" coordsize="21600,21600">
<v:path/>
<v:fill on="f" focussize="0,0"/>
<v:stroke on="f"/>
<v:imagedata src="wordml://1.png" o:title="Snipaste_2024-05-10_17-26-29"/>
<o:lock v:ext="edit" aspectratio="t"/>
<w10:wrap type="none"/>
<w10:anchorlock/>
</v:shape>
</w:pict>
</w:r>
</w:p>
</#list>
总结
- 有必要了解一些常用的标记及其说明,不必全懂,基础格式清晰即可。
- 不适合直接写ftl文件,用word另存的方式可以省下很多样式和排版的精力,只需另存后修改即可(格式化后发现很多无用的或不满足样式的代码,修剪一下吧)
- 如果word的视图不要求,可以采用html的文本“画”一个word,freemarker替换后生成的word打开默认是web视图(常规是页面视图)
- 有的图片过大会导致图片渲染失败,需要压缩后转base64
- thumbnailator 用于图片压缩
- webp-imageio 用于适配不同的图片格式,否则不支持的图片会报错
ImageIO.read(input)是null
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.8</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.sejda.imageio/webp-imageio 第三方库处理图片-->
<dependency>
<groupId>org.sejda.imageio</groupId>
<artifactId>webp-imageio</artifactId>
<version>0.1.6</version>
</dependency>
import lombok.SneakyThrows;
import net.coobird.thumbnailator.Thumbnails;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.Base64;
public class FileUtils {
public static boolean isImageFile(String fileName) {
if (fileName == null) {
return false;
}
// 获取文件扩展名
String extension = getFileExtension(fileName);
if (extension == null) {
return false;
}
// 检查扩展名是否在图片扩展名列表中
return extension.matches("(jpg|jpeg|png|gif|bmp|JPG|JPEG|PNG|GIF|BMP)$");
}
private static String getFileExtension(String fileName) {
if (fileName == null) {
return null;
}
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex > 0 && dotIndex < fileName.length() - 1) {
return fileName.substring(dotIndex + 1).toLowerCase();
}
return null;
}
@SneakyThrows
public static String convertImageToBase64(String imagePath) {
File file = new File(imagePath);
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(file);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
InputStream inputStream = null;
try {
inputStream = compressFile(fileInputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
return getBase64FromInputStream(inputStream);
}
//1-压缩图片
public static InputStream compressFile(InputStream input) throws IOException {
//1-压缩图片
BufferedImage bufImg = ImageIO.read(input);// 把图片读入到内存中
if(bufImg == null) {
return input;
}
bufImg = Thumbnails.of(bufImg).width(500).keepAspectRatio(true).outputQuality(1f).asBufferedImage();//压缩:宽度100px,长度自适应;质量压缩到0.8
ByteArrayOutputStream bos = new ByteArrayOutputStream();// 存储图片文件byte数组
ImageIO.write(bufImg, "jpg", bos); // 图片写入到 ImageOutputStream
input = new ByteArrayInputStream(bos.toByteArray());
// int available = input.available();
// //2-如果大小超过50KB,继续压缩
// if (available > 50000) {
// compressFile(input);
// }
return input;
}
//2-InputStream转化为base64
public static String getBase64FromInputStream(InputStream in) {
// 将图片文件转化为字节数组字符串,并对其进行Base64编码处理
byte[] data = null;
// 读取图片字节数组
try {
ByteArrayOutputStream swapStream = new ByteArrayOutputStream();
byte[] buff = new byte[100];
int rc = 0;
while ((rc = in.read(buff, 0, 100)) > 0) {
swapStream.write(buff, 0, rc);
}
data = swapStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
String str = new String(Base64.getEncoder().encode(data));
System.out.println("str length: " + str.length() + " str: " + str);
return str;
}
}
参考:
https://blog.csdn.net/weixin_45565886/article/details/131659741
https://blog.csdn.net/qq_34412985/article/details/96465187
https://blog.csdn.net/muhuixin123/article/details/135479978
https://blog.csdn.net/weixin_42313773/article/details/136271191
https://blog.csdn.net/qq_36635569/article/details/125223917