java生成docx文档, docx文档动态饼图

背景: 最近接了个需求, 要求生成日报,  大概如下图所示:

其中'*'表示变量, 看到要动态生成doc给我难受坏了,为什么会有这种需求?

然后看到里面还要动态生成饼图, oh, no.........没有办法, 硬着头皮上吧.

于是就搜了下java生成docx的方式, 看到的, 比较靠谱的一种通过freemaker生成, 替换其中的动态数据即可.

这种的好处就是提供一个模板, 替换完里面的数据之后格式不会乱, 于是愉快的决定就用这个了, 过程如下

1.让产品给一个最终的日报文档, 然后另存为xml文件, 没错, 就是xml文件, 放着备用

2.项目中加入freemaker依赖及相关配置

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
    <version>2.5.0</version>
</dependency>
spring:
  freemarker:
    cache: false  #关闭模板缓存,方便测试
    settings:
      template_update_delay: 0 #检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试
    suffix: .ftl               #指定Freemarker模板文件的后缀名
    enabled: true
    expose-spring-macro-helpers: true

3.开始搞利用模板生成docx的代码

3.1 在resources文件夹下新建 templates 文件夹, 将第1步docx转存的xml文件复制到该目录下,并更改文件后缀为 .ftl (文件后缀和配置里保持一致即可), 注意如果想用Microsoft office打开的话就用doc文件转xml

        之后对这个模板文件的代码进行格式化一下(方便找需要替换的地方), 找到需要替换的地方之后,使用${变量}进行替换就行了, 这里都是freemaker的基础操作, 就是docx的结构有点麻烦, 不过xml文件仔细看看很容易就发现规律了. 大部分都是这种, 样式后面跟着文字, 如果遇到表格什么的可能稍微不一样.

3.2 写一下服务类


public interface TemplateService {
    /**
     * 创建模板文件
     * @param outputPath 输出路径
     * @param templateFile 模板文件名
     * @param param 参数
     * @return 是否创建成功
     */
    boolean createTemplateFile(String outputPath,String fileName, String templateFile, Map<String, Object> param);

}

@Slf4j
@Service
public class TemplateServiceImpl implements TemplateService {

    @Autowired
    private FreeMarkerConfigurer freeMarkerConfigurer;


    @Override
    public boolean createTemplateFile(String outputPath, String fileName, String templateFile, Map<String, Object> param) {
        Configuration cfg = freeMarkerConfigurer.getConfiguration();
        Writer out = null;

        File file = new File(outputPath);
        if (!file.exists()) {
            file.mkdirs();
        }
        file = new File(outputPath + fileName);
        try {
            cfg.setDefaultEncoding(StandardCharsets.UTF_8.name());
            Template template = cfg.getTemplate(templateFile);
            out = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8);

            //将数据放在map里,然后就会自动渲染模版
            template.process(param, out);
            return true;
        } catch (Exception e) {
            log.error("TemplateServiceImpl#createTemplateFile生成文件失败............." + e);
        } finally {
            if (out != null) {
                try {
                    out.flush();
                    out.close();
                } catch (IOException e) {
                    log.error("TemplateServiceImpl#createTemplateFile流关闭失败..........." + e);
                }
            }
        }
        return false;
    }


}

3.3 写个测试的controller调用试一试

    @GetMapping("/createdoc")
    public String createdoc() throws Exception {
        //1.文档里要动态替换的数据
        Map<String, Object> param = new HashMap<>();
        String company = "测试工作室";
        param.put("company", company);//公司
        param.put("time", "2023-12-10 11:00 - 2023-12-11 11:00");//日报时间
        param.put("order", 4500);//订单数
        param.put("amount", 304614.23);//金额
        

        //2.根据模板生成docx文档
        //用来输出生成的文档目录
        String outputPath = "D:/data/hotspot/docx/";
        String dayStr = DateTimeUtil.getDayStr(convertToDateTime(new Date()));
        String fileName = company + "电商日报【" + dayStr + "】.docx";
        templateService.createTemplateFile(outputPath, fileName, "模板.ftl", param);
        
        //3.到这里,文档就已经生成了, 去目录下看生成的文档是否符合要求就行了
        //我这里后面又把文件上传到oss上面去了, 然后将数据更新到数据库了
        String fileAbsolutePath = outputPath + fileName;
        //3.1 todo 上传到oss, 并将url更新到数据库

        //3.2 删除本地机器的文件
        File file = new File(fileAbsolutePath);
        boolean delete = file.delete();

        return "success";
    }

4.文字是没问题了, 那么图片怎么解决呢? 还要动态生成, 还要写到docx里面去. 

   于是开始发功, 百度大法....... 找到了jfreechart这个框架, 话不多说, 开始干活.

4.1 依赖

        <!--用于jfreechart生成图片  -->
        <dependency>
            <groupId>org.jfree</groupId>
            <artifactId>jfreechart</artifactId>
            <version>1.5.0</version>
        </dependency>
        <!--非必要 -这个里面少一些内置包 版本比较高,需要单独引入-->
        <dependency>
            <groupId>com.guicedee.services</groupId>
            <artifactId>jfreechart</artifactId>
            <version>1.1.1.5-jre15</version>
        </dependency>

4.2 jfree工具类


import org.apache.commons.lang3.StringUtils;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartUtils;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.StandardChartTheme;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.CategoryLabelPositions;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.block.BlockBorder;
import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
import org.jfree.chart.labels.StandardPieSectionLabelGenerator;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PiePlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.BarRenderer;
import org.jfree.chart.renderer.category.LineAndShapeRenderer;
import org.jfree.chart.ui.HorizontalAlignment;
import org.jfree.chart.ui.RectangleInsets;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.general.DefaultPieDataset;

import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.math.RoundingMode;
import java.rmi.server.ExportException;
import java.text.NumberFormat;
import java.util.Iterator;
import java.util.Map;

/**
 * 创建图集图表
 */
public class JfreeUtil {

    private static final Font FONT = new Font("宋体", Font.PLAIN, 12);
    private static StandardChartTheme defaultTheme(){
        //创建主题样式
        StandardChartTheme theme=new StandardChartTheme("CN");
        //设置标题字体
        theme.setExtraLargeFont(new Font("隶书",Font.BOLD,20));
        //设置图例的字体
        theme.setRegularFont(new Font("宋书",Font.PLAIN,15));
        //设置轴向的字体
        theme.setLargeFont(new Font("宋书",Font.PLAIN,15));
        return theme;
    }
    public static StandardChartTheme createChartTheme(String fontName) {
        StandardChartTheme theme = new StandardChartTheme("unicode") {
            public void apply(JFreeChart chart) {
                chart.getRenderingHints().put(RenderingHints.KEY_TEXT_ANTIALIASING,
                        RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
                super.apply(chart);
            }
        };
        fontName = StringUtils.isBlank(fontName) ? "宋体" : fontName;
        theme.setExtraLargeFont(new Font(fontName, Font.PLAIN, 20));
        theme.setLargeFont(new Font(fontName, Font.PLAIN, 15));
        theme.setRegularFont(new Font(fontName, Font.PLAIN, 15));
        theme.setSmallFont(new Font(fontName, Font.PLAIN, 10));
        return theme;
    }


    public static String createPieChart(String title, Map<String, Double> datas, int width, int height) throws IOException {
        //根据jfree生成一个本地饼状图
        DefaultPieDataset pds = new DefaultPieDataset();
        datas.forEach(pds::setValue);
        //应用主题样式
        ChartFactory.setChartTheme(createChartTheme(""));
        //图标标题、数据集合、是否显示图例标识、是否显示tooltips、是否支持超链接
        JFreeChart chart = ChartFactory.createPieChart(title, pds, true, false, false);
        chart.getTitle().setFont(FONT);
        chart.getLegend().setItemFont(FONT);
        //设置抗锯齿
        chart.setTextAntiAlias(false);

        PiePlot plot = (PiePlot) chart.getPlot();
        plot.setStartAngle(90);
        plot.setNoDataMessage("暂无数据");
        plot.setNoDataMessagePaint(Color.blue); // 设置无数据时的信息显示颜色

        //忽略无值的分类
        plot.setIgnoreNullValues(true);
        plot.setIgnoreZeroValues(true);
        plot.setBackgroundAlpha(0f);
        //设置标签阴影颜色
        plot.setShadowPaint(new Color(255, 255, 255));

        //设置标签是否显示在饼块内部,默认时在外部
//        plot.setSimpleLabels(true);

        chart.getLegend().setHorizontalAlignment(HorizontalAlignment.CENTER);//设置水平对齐 左对齐;
        chart.getLegend().setMargin(0, 0, 0, 0);//参数是:上,左,下,右. 设置饼图的位置
        chart.getLegend().setPadding(0, 0, 20, 0);// 设置饼图下文字的位置
        chart.getLegend().setFrame(new BlockBorder(0, 0, 0, 0));// 设置饼图下文字边框的位置
        // 图片中显示百分比:自定义方式,{0} 表示选项, {1} 表示数值, {2} 表示所占比例,小数点后两位
        NumberFormat percentInstance = NumberFormat.getPercentInstance();
        percentInstance.setRoundingMode(RoundingMode.HALF_UP);
        percentInstance.setMaximumFractionDigits(2);
        plot.setLabelGenerator(new StandardPieSectionLabelGenerator("{0},{2}", NumberFormat.getNumberInstance(), percentInstance));

        return createFile(chart, width, height);
    }

    public static String createBarChart(String title, Map<String, Object> datas, String type, String units, PlotOrientation orientation, int width, int height) throws IOException {
        //数据集
        DefaultCategoryDataset ds = new DefaultCategoryDataset();
        Iterator<Map.Entry<String, Object>> iterator = datas.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, Object> entry = iterator.next();
            ds.setValue(Double.valueOf(String.valueOf(entry.getValue())), "工单数量", StringUtils.defaultString(entry.getKey(), ""));
        }

        //创建柱状图,柱状图分水平显示和垂直显示两种
        JFreeChart chart = ChartFactory.createBarChart(title, type, units, ds, orientation, false, false, false);

        //设置文本抗锯齿,防止乱码
        chart.setTextAntiAlias(false);
        //得到绘图区
        CategoryPlot plot = (CategoryPlot) chart.getPlot();
        plot.setNoDataMessage("no data");
        //设置柱的透明度
        plot.setForegroundAlpha(1.0f);
        plot.setOutlineVisible(false);

        //获取X轴的对象
        CategoryAxis categoryAxis = plot.getDomainAxis();
        //坐标轴标尺值是否显示
        categoryAxis.setTickLabelsVisible(true);
        //坐标轴标尺是否显示
        categoryAxis.setTickMarksVisible(false);
        categoryAxis.setTickLabelFont(FONT);
        categoryAxis.setTickLabelPaint(Color.BLACK);
        categoryAxis.setLabelFont(FONT);// X轴标题
        //categoryAxis.setCategoryLabelPositionOffset(2);//图表横轴与标签的距离(10像素)

        //获取Y轴对象
        ValueAxis valueAxis = plot.getRangeAxis();
        valueAxis.setTickLabelsVisible(true);
        valueAxis.setTickMarksVisible(false);
        valueAxis.setUpperMargin(0.15);//设置最高的一个柱与图片顶端的距离(最高柱的20%)
        valueAxis.setLowerMargin(0d);
        valueAxis.setTickLabelFont(FONT);//Y轴数值
        valueAxis.setLabelPaint(Color.BLACK);//字体颜色
        valueAxis.setLabelFont(FONT);//Y轴标题

        NumberAxis numberAxis = (NumberAxis) plot.getRangeAxis();
        //设置Y轴刻度为整数
        numberAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());

        //设置网格横线颜色
        plot.setRangeGridlinePaint(Color.gray);
        plot.setRangeGridlinesVisible(true);
        //图片背景色
        plot.setBackgroundPaint(Color.white);
        plot.setOutlineVisible(false);

        // 设置原点xy轴相交,柱子从横轴开始,否则会有间隙
        plot.setAxisOffset(new RectangleInsets(0d, 0d, 0d, 0d));
        //设置网格横线大小
        plot.setDomainGridlineStroke(new BasicStroke(0.5F));
        plot.setRangeGridlineStroke(new BasicStroke(0.5F));
        //设置柱状图柱子相关
        CategoryPlot categoryPlot = chart.getCategoryPlot();
        BarRenderer rendererBar = (BarRenderer) categoryPlot.getRenderer();

        //组内柱子间隔为组宽的10%,调整柱子宽度
        rendererBar.setItemMargin(0.6);
        rendererBar.setMaximumBarWidth(0.07);

        rendererBar.setDrawBarOutline(true);
        rendererBar.setSeriesOutlinePaint(0, Color.decode("#4F97D5"));
        //设置柱的颜色#5B9BE6
        rendererBar.setSeriesPaint(0, Color.decode("#4F97D5"));

        //设置柱子上显示值
        rendererBar.setDefaultItemLabelGenerator(new StandardCategoryItemLabelGenerator());
        rendererBar.setDefaultItemLabelFont(FONT);
        rendererBar.setDefaultItemLabelsVisible(true);
        rendererBar.setDefaultItemLabelPaint(Color.BLACK);

        return createFile(chart, width, height);

    }

    public static String createLineChart(String title, Map<String, Object> datas, String type, String unit, PlotOrientation orientation, int width, int hight) throws IOException {
        DefaultCategoryDataset ds = new DefaultCategoryDataset();
        Iterator iterator = datas.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry) iterator.next();
            ds.setValue(Double.valueOf(String.valueOf(entry.getValue())), "工单数量", entry.getKey().toString());
        }

        //创建折线图,折线图分水平显示和垂直显示两种
        JFreeChart chart = ChartFactory.createLineChart(title, type, unit, ds, orientation, false, true, true);
        //设置文本抗锯齿,防止乱码
        chart.setTextAntiAlias(false);
        //chart.setBorderVisible(true);

        //得到绘图区
        CategoryPlot plot = (CategoryPlot) chart.getPlot();
        //设置横轴标签项字体
        CategoryAxis categoryAxis = plot.getDomainAxis();
        categoryAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_45);
        categoryAxis.setTickMarksVisible(false);
        categoryAxis.setTickLabelsVisible(true);
        categoryAxis.setTickLabelFont(new Font("宋体", Font.PLAIN, 12));
        categoryAxis.setLabelFont(new Font("宋体", Font.PLAIN, 12));

        ValueAxis valueAxis = plot.getRangeAxis();
        valueAxis.setTickMarksVisible(false);
        valueAxis.setTickLabelsVisible(true);
        valueAxis.setTickLabelFont(new Font("宋体", Font.PLAIN, 12));
        valueAxis.setLabelFont(new Font("宋体", Font.PLAIN, 12));

        NumberAxis numberAxis = (NumberAxis) plot.getRangeAxis();
        //设置Y轴刻度跨度
        numberAxis.setUpperMargin(0.15);
        numberAxis.setLowerMargin(0);
        numberAxis.setAutoRangeMinimumSize(5);

        // 设置背景透明度
        plot.setBackgroundAlpha(0.1f);
        plot.setForegroundAlpha(1.0f);
        // 设置网格横线颜色
        plot.setRangeGridlinePaint(Color.gray);
        // 设置网格横线大小
        plot.setDomainGridlineStroke(new BasicStroke(0.5F));
        plot.setRangeGridlineStroke(new BasicStroke(0.5F));
        plot.setBackgroundPaint(Color.white);
        plot.setOutlineVisible(false);

        // 设置原点xy轴相交,y轴为0时,点在横坐标上,否则不在横坐标上
        plot.setAxisOffset(new RectangleInsets(0d, 0d, 0d, 0d));

        // 生成折线图上的数字
        //绘图区域(红色矩形框的部分)
        LineAndShapeRenderer renderer = (LineAndShapeRenderer) plot.getRenderer();
        renderer.setDefaultItemLabelGenerator(new StandardCategoryItemLabelGenerator());
        //设置图表上的数字可见
        renderer.setDefaultItemLabelsVisible(true);
        //设置图表上的数字字体
        renderer.setDefaultItemLabelFont(new Font("宋体", Font.PLAIN, 12));

        // 设置线条是否被显示填充颜色
        renderer.setUseFillPaint(true);
        renderer.setSeriesStroke(0, new BasicStroke(4.0f));
        renderer.setSeriesPaint(0, Color.decode("#4472C4"));

        return createFile(chart, width, hight);
    }

    public static String createFile(JFreeChart chart, int width, int hight) throws IOException {
        File templateFile = File.createTempFile("jfreetemp", ".png");
        String filePath = templateFile.getParent() + File.separator + templateFile.getName();
        try {
            if (templateFile.exists()) {
                templateFile.delete();
            }
            ChartUtils.saveChartAsPNG(templateFile, chart, width, hight);
        } catch (IOException e) {
            throw new ExportException("创建图表文件失败!");
        }

        return filePath;
    }


}

4.2 对图片进行base64编码的工具类, 因为我们的模板是由docx另存为xml文件, 然后又改的.ftl后缀,

但其本质仍然是xml文件, 那么我们就相当于要往xml文件中写图片,就需要对图片进行编码了, 注意这里的图片在Microsoft office中打不开, 不支持, 目前这个问题使用freemaker还没有找到更好的方法解决, 但是可以改用poi, 相关参考blog如下  poi生成word文档.


import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.util.Base64;

public class EncodeUtil {
    public static String readImage(String str_FileName) {
        BufferedInputStream bis = null;
        byte[] bytes = null;
        try {
            try {
                bis = new BufferedInputStream(new FileInputStream(str_FileName));
                bytes = new byte[bis.available()];
                bis.read(bytes);
            } finally {
                bis.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return encryptBASE64(bytes);
    }
    public static String readImage(FileInputStream in) {
        BufferedInputStream bis = null;
        byte[] bytes = null;
        try {
            try {
                bis = new BufferedInputStream(in);
                bytes = new byte[bis.available()];
                bis.read(bytes);
            } finally {
                bis.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return encryptBASE64(bytes);
    }
    public static String encryptBASE64(byte[] key) {
        Base64.Encoder encoder= Base64.getMimeEncoder();
        return encoder.encodeToString(key);
    }

4.3 重新往测试的controller写下代码

    @GetMapping("/createdoc")
    public String createdoc() throws Exception {
        //1.文档里要动态替换的数据
        Map<String, Object> param = new HashMap<>();
        String company = "测试工作室";
        param.put("company", company);//公司
        param.put("time", "2023-12-10 11:00 - 2023-12-11 11:00");//日报时间
        param.put("order", 4500);//订单数
        param.put("amount", 304614.23);//金额
        //1.1 图片数据
        Map<String, Double> map = new LinkedHashMap<>();
        map.put("美食",53.00);
        map.put("箱包",23.00);
        map.put("运动",13.11);
        map.put("衣服",73.25);
        map.put("其他",40.36);
        map.put("宠物",3.21);
        //使用Jfreechart创建饼图
        String pictureUrl = JfreeUtil.createPieChart("", doubleMap, 600, 500);
        //对图片进行编码,转换为string类型
        FileInputStream inputStream = new FileInputStream(pictureUrl);
        String image = EncodeUtil.readImage(inputStream);
        param.put("image", image);//图片.....将模板里的图片(很长的一串)替换为 ${image}即可

        //2.根据模板生成docx文档
        //用来输出生成的文档目录
        String outputPath = "D:/data/hotspot/docx/";
        String dayStr = DateTimeUtil.getDayStr(convertToDateTime(new Date()));
        String fileName = company + "电商日报【" + dayStr + "】.docx";
        templateService.createTemplateFile(outputPath, fileName, "模板.ftl", param);
        
        //3.到这里,文档就已经生成了, 去目录下看生成的文档是否符合要求就行了
        //我这里后面又把文件上传到oss上面去了, 然后将数据更新到数据库了
        String fileAbsolutePath = outputPath + fileName;
        //3.1 todo 上传到oss, 并将url更新到数据库

        //3.2 删除本地机器的文件
        File file = new File(fileAbsolutePath);
        boolean delete = file.delete();

        return "success";
    }

一般图片是放在如下标签里的.

到此结束, 生成的文档和需求里的差不多, 就是这个饼图有点丑.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值