Java generates docx document, docx document dynamic pie chart

Background: I recently received a request to generate a daily report, as shown below:

‘*’ represents a variable. It makes me uncomfortable to see that doc needs to be generated dynamically. Why is there such a need?

Then I saw that there was a need to dynamically generate a pie chart, oh, no…there is no other way, just bite the bullet and do it.

So I searched for ways to generate docx in Java, and I saw that a more reliable one is generated through freemaker, and just replace the dynamic data in it.

The advantage of this is that it provides a template. After replacing the data inside, the format will not be messy, so I happily decided to use this. The process is as follows

1. Let the product give you a final daily report document, and then save it as an xml file. Yes, it is an xml file. Keep it for later use.

2. Add freemaker dependencies and related configurations to the project

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
    <version>2.5.0</version>
</dependency>
spring:
  freemarker:
    cache: false #Turn off template caching to facilitate testing
    settings:
      template_update_delay: 0 #Check the template update delay time. Set to 0 to check immediately. If the time is greater than 0, caching will be inconvenient for template testing.
    suffix: .ftl #Specify the suffix name of the Freemarker template file
    enabled: true
    expose-spring-macro-helpers: true

3. Start working on using templates to generate docx code

3.1 Create a new templates folder under the resources folder, copy the xml file transferred from docx in step 1 to this directory, and change the file suffix to .ftl (the file suffix should be consistent with the configuration). Note that if you want to use If you open Microsoft office, use the doc file to convert it to xml.

After that, format the code of this template file (to facilitate finding the places that need to be replaced). After finding the places that need to be replaced, just use ${variables} to replace them. These are the basic operations of freemaker, but the structure of docx is a bit Troublesome, but if you look carefully at the xml file, you can easily find the pattern. Most of them are like this, with the style followed by text. If you encounter a table or something, it may be slightly different.

3.2 Write the service class

public interface TemplateService {
    /**
     * Create template file
     * @param outputPath output path
     * @param templateFile template file name
     * @param param parameter
     * @return Whether the creation is successful
     */
    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);

            //Put the data in the map, and then the template will be automatically rendered.
            template.process(param, out);
            return true;
        } catch (Exception e) {
            log.error("TemplateServiceImpl#createTemplateFile failed to generate file..." + e);
        } finally {
            if (out != null) {
                try {
                    out.flush();
                    out.close();
                } catch (IOException e) {
                    log.error("TemplateServiceImpl#createTemplateFile stream closing failed..." + e);
                }
            }
        }
        return false;
    }


}

3.3 Write a test controller call and give it a try

 @GetMapping("/createdoc")
    public String createdoc() throws Exception {
        //1. Data to be dynamically replaced in the document
        Map<String, Object> param = new HashMap<>();
        String company = "Test Studio";
        param.put("company", company);//Company
        param.put("time", "2023-12-10 11:00 - 2023-12-11 11:00");//Daily report time
        param.put("order", 4500);//Number of orders
        param.put("amount", 304614.23);//Amount
        

        //2. Generate docx document based on template
        //Used to output the generated document directory
        String outputPath = "D:/data/hotspot/docx/";
        String dayStr = DateTimeUtil.getDayStr(convertToDateTime(new Date()));
        String fileName = company + "E-commerce Daily【" + dayStr + "】.docx";
        templateService.createTemplateFile(outputPath, fileName, "template.ftl", param);
        
        //3. At this point, the document has been generated. Just go to the directory to see if the generated document meets the requirements.
        //I uploaded the file to OSS later, and then updated the data to the database.
        String fileAbsolutePath = outputPath + fileName;
        //3.1 Upload todo to oss, and update the url to the database

        //3.2 Delete files on the local machine
        File file = new File(fileAbsolutePath);
        boolean delete = file.delete();

        return "success";
    }

4. The text is no problem, but how to solve the problem of pictures? It needs to be generated dynamically and written into docx.

So I started to work hard, Baidu Dafa… I found the jfreechart framework, didn’t say much, and started working.

4.1 Dependencies

 <!--Used for jfreechart to generate pictures -->
        <dependency>
            <groupId>org.jfree</groupId>
            <artifactId>jfreechart</artifactId>
            <version>1.5.0</version>
        </dependency>
        <!--Not necessary - There are fewer built-in packages in this one. The version is relatively high and needs to be introduced separately-->
        <dependency>
            <groupId>com.guicedee.services</groupId>
            <artifactId>jfreechart</artifactId>
            <version>1.1.1.5-jre15</version>
        </dependency>

4.2 jfree tool class

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;

/**
 * Create atlas charts
 */
public class JfreeUtil {

    private static final Font FONT = new Font("宋体", Font.PLAIN, 12);
    private static StandardChartTheme defaultTheme(){
        //Create theme style
        StandardChartTheme theme=new StandardChartTheme("CN");
        //Set title font
        theme.setExtraLargeFont(new Font("official script",Font.BOLD,20));
        //Set the font of the legend
        theme.setRegularFont(new Font("Song Shu",Font.PLAIN,15));
        //Set the axial font
        theme.setLargeFont(new Font("Song Shu",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 {
        //Generate a local pie chart based on jfree
        DefaultPieDataset pds = new DefaultPieDataset();
        datas.forEach(pds::setValue);
        //Apply theme style
        ChartFactory.setChartTheme(createChartTheme(""));
        //Icon title, data collection, whether to display legend logo, whether to display tooltips, whether to support hyperlinks
        JFreeChart chart = ChartFactory.createPieChart(title, pds, true, false, false);
        chart.getTitle().setFont(FONT);
        chart.getLegend().setItemFont(FONT);
        //Set anti-aliasing
        chart.setTextAntiAlias(false);

        PiePlot plot = (PiePlot) chart.getPlot();
        plot.setStartAngle(90);
        plot.setNoDataMessage("No data yet");
        plot.setNoDataMessagePaint(Color.blue); //Set the color of information display when there is no data

        //Ignore unvalued categories
        plot.setIgnoreNullValues(true);
        plot.setIgnoreZeroValues(true);
        plot.setBackgroundAlpha(0f);
        //Set label shadow color
        plot.setShadowPaint(new Color(255, 255, 255));

        //Set whether the label is displayed inside the pie block. It is outside by default.
// plot.setSimpleLabels(true);

        chart.getLegend().setHorizontalAlignment(HorizontalAlignment.CENTER);//Set horizontal alignment left alignment;
        chart.getLegend().setMargin(0, 0, 0, 0);//The parameters are: top, left, bottom, right. Set the position of the pie chart
        chart.getLegend().setPadding(0, 0, 20, 0);//Set the position of the text under the pie chart
        chart.getLegend().setFrame(new BlockBorder(0, 0, 0, 0));//Set the position of the text border under the pie chart
        // Percentage displayed in the picture: customized method, {0} represents the option, {1} represents the value, {2} represents the proportion, two decimal places
        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 {
        //data set
        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())), "Quantity of work orders", StringUtils.defaultString(entry.getKey(), ""));
        }

        //Create a histogram. The histogram is divided into two types: horizontal display and vertical display.
        JFreeChart chart = ChartFactory.createBarChart(title, type, units, ds, orientation, false, false, false);

        //Set text anti-aliasing to prevent garbled characters
        chart.setTextAntiAlias(false);
        //Get the drawing area
        CategoryPlot plot = (CategoryPlot) chart.getPlot();
        plot.setNoDataMessage("no data");
        //Set the transparency of the column
        plot.setForegroundAlpha(1.0f);
        plot.setOutlineVisible(false);

        //Get the X-axis object
        CategoryAxis categoryAxis = plot.getDomainAxis();
        //Whether the axis scale value is displayed
        categoryAxis.setTickLabelsVisible(true);
        //Whether the coordinate axis ruler is displayed
        categoryAxis.setTickMarksVisible(false);
        categoryAxis.setTickLabelFont(FONT);
        categoryAxis.setTickLabelPaint(Color.BLACK);
        categoryAxis.setLabelFont(FONT);//X-axis title
        //categoryAxis.setCategoryLabelPositionOffset(2);//The distance between the horizontal axis of the chart and the label (10 pixels)

        //Get the Y-axis object
        ValueAxis valueAxis = plot.getRangeAxis();
        valueAxis.setTickLabelsVisible(true);
        valueAxis.setTickMarksVisible(false);
        valueAxis.setUpperMargin(0.15);//Set the distance between the highest bar and the top of the picture (20% of the highest bar)
        valueAxis.setLowerMargin(0d);
        valueAxis.setTickLabelFont(FONT);//Y-axis value
        valueAxis.setLabelPaint(Color.BLACK);//Font color
        valueAxis.setLabelFont(FONT);//Y axis title

        NumberAxis numberAxis = (NumberAxis) plot.getRangeAxis();
        //Set the Y-axis scale to an integer
        numberAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());

        //Set grid horizontal line color
        plot.setRangeGridlinePaint(Color.gray);
        plot.setRangeGridlinesVisible(true);
        //Picture background color
        plot.setBackgroundPaint(Color.white);
        plot.setOutlineVisible(false);

        //Set the origin to intersect the xy axis, and the column starts from the horizontal axis, otherwise there will be a gap
        plot.setAxisOffset(new RectangleInsets(0d, 0d, 0d, 0d));
        //Set grid horizontal line size
        plot.setDomainGridlineStroke(new BasicStroke(0.5F));
        plot.setRangeGridlineStroke(new BasicStroke(0.5F));
        //Set the histogram column related
        CategoryPlot categoryPlot = chart.getCategoryPlot();
        BarRenderer rendererBar = (BarRenderer) categoryPlot.getRenderer();

        //The spacing between columns within the group is 10% of the group width, adjust the column width
        rendererBar.setItemMargin(0.6);
        rendererBar.setMaximumBarWidth(0.07);

        rendererBar.setDrawBarOutline(true);
        rendererBar.setSeriesOutlinePaint(0, Color.decode("#4F97D5"));
        //Set the column color #5B9BE6
        rendererBar.setSeriesPaint(0, Color.decode("#4F97D5"));

        //Set the value displayed on the column
        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())), "Quantity of work orders", entry.getKey().toString());
        }

        //Create a line chart. The line chart is divided into horizontal display and vertical display.
        JFreeChart chart = ChartFactory.createLineChart(title, type, unit, ds, orientation, false, true, true);
        //Set text anti-aliasing to prevent garbled characters
        chart.setTextAntiAlias(false);
        //chart.setBorderVisible(true);

        //Get the drawing area
        CategoryPlot plot = (CategoryPlot) chart.getPlot();
        //Set the font of the horizontal axis label item
        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();
        //Set Y-axis scale span
        numberAxis.setUpperMargin(0.15);
        numberAxis.setLowerMargin(0);
        numberAxis.setAutoRangeMinimumSize(5);

        //Set background transparency
        plot.setBackgroundAlpha(0.1f);
        plot.setForegroundAlpha(1.0f);
        //Set grid horizontal line color
        plot.setRangeGridlinePaint(Color.gray);
        //Set the grid horizontal line size
        plot.setDomainGridlineStroke(new BasicStroke(0.5F));
        plot.setRangeGridlineStroke(new BasicStroke(0.5F));
        plot.setBackgroundPaint(Color.white);
        plot.setOutlineVisible(false);

        //Set the origin point where the xy axis intersects. When the y axis is 0, the point is on the abscissa, otherwise it is not on the abscissa.
        plot.setAxisOffset(new RectangleInsets(0d, 0d, 0d, 0d));

        // Generate numbers on the line chart
        //Drawing area (part of the red rectangle)
        LineAndShapeRenderer renderer = (LineAndShapeRenderer) plot.getRenderer();
        renderer.setDefaultItemLabelGenerator(new StandardCategoryItemLabelGenerator());
        //Set the numbers on the chart to be visible
        renderer.setDefaultItemLabelsVisible(true);
        //Set the number font on the chart
        renderer.setDefaultItemLabelFont(new Font("宋体", Font.PLAIN, 12));

        // Set whether the line is displayed with fill color
        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("Failed to create chart file!");
        }

        return filePath;
    }


}

4.2 Tool class for base64 encoding of images, because our template is saved as an xml file from docx, and then changed to the .ftl suffix.

But its essence is still an xml file, so we are equivalent to writing pictures into the xml file, and we need to encode the pictures. Note that the pictures here cannot be opened in Microsoft Office and are not supported. Currently, this problem can still be solved using freemaker. I haven’t found a better way to solve it, but you can use poi instead. For related reference, the following blog post uses poi to generate word documents.

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 Rewrite the code to the test controller

 @GetMapping("/createdoc")
    public String createdoc() throws Exception {
        //1. Data to be dynamically replaced in the document
        Map<String, Object> param = new HashMap<>();
        String company = "Test Studio";
        param.put("company", company);//Company
        param.put("time", "2023-12-10 11:00 - 2023-12-11 11:00");//Daily report time
        param.put("order", 4500);//Number of orders
        param.put("amount", 304614.23);//Amount
        //1.1 Image data
        Map<String, Double> map = new LinkedHashMap<>();
        map.put("Gourmet",53.00);
        map.put("Luggage",23.00);
        map.put("Motion",13.11);
        map.put("clothes",73.25);
        map.put("Other",40.36);
        map.put("Pet",3.21);
        //Use Jfreechart to create a pie chart
        String pictureUrl = JfreeUtil.createPieChart("", doubleMap, 600, 500);
        //Encode the image and convert it to string type
        FileInputStream inputStream = new FileInputStream(pictureUrl);
        String image = EncodeUtil.readImage(inputStream);
        param.put("image", image);//Picture...replace the picture (a long string) in the template with ${image}

        //2. Generate docx document based on template
        //Used to output the generated document directory
        String outputPath = "D:/data/hotspot/docx/";
        String dayStr = DateTimeUtil.getDayStr(convertToDateTime(new Date()));
        String fileName = company + "E-commerce Daily【" + dayStr + "】.docx";
        templateService.createTemplateFile(outputPath, fileName, "template.ftl", param);
        
        //3. At this point, the document has been generated. Just go to the directory to see if the generated document meets the requirements.
        //I uploaded the file to OSS later, and then updated the data to the database.
        String fileAbsolutePath = outputPath + fileName;
        //3.1 Upload todo to oss, and update the url to the database

        //3.2 Delete files on the local machine
        File file = new File(fileAbsolutePath);
        boolean delete = file.delete();

        return "success";
    }

Generally, pictures are placed in the following tags.

At the end of this point, the generated document is similar to the one in the requirements, except that the pie chart is a bit ugly.