【生成PDF】Java如何根据前台Echarts图表生成PDF,并下载

本文介绍了如何使用Java的iTextPDF库生成PDF,并结合前端Echarts图表的信息,将图片插入PDF中。首先,通过Echarts的getDataURL方法获取图片的base64编码,然后在后台解码并插入PDF。此外,还展示了如何创建PDF表格和处理图片缩放。最后,通过HTTP响应直接下载生成的PDF文件。
摘要由CSDN通过智能技术生成


前言

提示:本文仅用于记录日常,多有不足,仅供参考。

这两天遇到个任务,要将前台已有的echarts图表和一些数据整合,生成一个报表,且以PDF的格式自动下载保存。
前台的数据已经有了。这意味着,所有的数据源都可以直接拿到,那么现在要解决的问题只有以下3点:

  1. 如何通过java代码生成PDF?
  2. 如何在PDF中画前台的echarts图?
  3. 如何将生成的PDF自动下载下来?

一、如何通过java代码生成PDF?

1.依赖

    查看了相关资料,和尝试,最终用来生成PDF的依赖只用了一个,如下:

<dependency>
   <groupId>com.itextpdf</groupId>
    <artifactId>itextpdf</artifactId>
    <version>5.5.13.3</version>
</dependency> 

二、如何在PDF中画前台的echarts图?

    前面提到,所有的数据和图表的样子都已经再前台展示了。
那么,我的第一反应是,若我能直接从前台将echarts图的信息拿到,传给后台,再从后台将这个图片信息写到PDF中就可以啦。

1.如何拿到前台echarts图的信息?

    我发现,前台在写echarts的时候,有一个配置项toolbox,当show:true设置其显示时,会看见一个下载按钮。点击后可以直接下载这个echarts图片。

前台代码:
var myChart1;
//加载实时调用
function loadChart1(res)
{
    var colorList = ['#73DDFF', '#73ACFF'];
    myChart1 = echarts.init(document.getElementById('chart-visitTrend'));
    option = {
            /* title: {
              text: '审计调用统计'
            }, */
            tooltip: {
              trigger: 'axis'
            },
            legend: {
              data: ['接收', '发送']
            },
            grid: {
                top:'5%',
              left: '1%',
              right: '3%',
              bottom: '0%',
              containLabel: true
            },
            /* toolbox: { //保存图片
              show: true,
              orient: "vertical",
              itemSize: 12,
              feature: {
                saveAsImage: {
                    name:"实时调用下载"
                }
              }
            }, */
            xAxis: {
              type: 'category',
              boundaryGap: false,
              //data: ['2022-01-01', '2022-01-02', '2022-01-03', '2022-01-04', '2022-01-05', '2022-01-06', '2022-01-07']
              data:res.timeData
            },
            yAxis: {
              type: 'value',
              axisLabel: {
                  formatter: '{value} 次'
                }
            },
            series: [
              {
                //name: '',
                type: 'line',
                stack: 'Total',
                color: colorList[0],
                //data: [120, 132, 101, 134, 90, 230, 210]
                data:res.countData
              }
            ]
          };
    // 使用刚指定的配置项和数据显示图表。
    myChart1.clear();
    myChart1.setOption(option);
}

    既然前台可以直接下载,说明一定可以直接从前台获取这个图片的信息。可以通过getDataURL()方法获取这个图片信息。直接看,前台“导出”按钮的点击事件。

// 导出
doExport = function()
{
    //获取echarts图的base64编码,为png格式。
    var picBase64Info = myChart1.getDataURL();
    var con = getCondition();//其它条件
    con['picBase64Info'] = picBase64Info;

    var form =document.createElement("form");
    form.style.display='none';
    form.action ='/reportform/exportform';
    form.method = 'post';
    document.body.append(form);
    
    for(var key in con)
    {
        var input = document.createElement("input");
        input.type = "hidden";
        input.name = key;
        input.value = con[key];
        form.target = "exportFrame";
        form.appendChild(input);
    }
    form.submit();
    form.remove();

    // 以下通过ajax请求方式行不通,无法触发浏览器的下载动作
/*  
    $.ajax({
        url: '/reportform/exportform',
        type: "post",
        async: true,
        cache: false,
        data: JSON.stringify(con),
        contentType : "application/json",
        dataType: "json",
        error: function(xhr)
        {
           // 操作失败
        }
    }); */
}

    由上可见,picBase64Info 就是那张echarts图片经过base64加密后的信息。直接将它传递到后台,进行base64解密即可。忽略getCondition();操作,这只是我想携带的一些别的数据到后台。

提示: 由上面注释的内容可以看出,我没有直接通过ajax向后台发起请求的方式将图片信息携带到后台。原因是,这样浏览器无法触发下载动作。经尝试,使用一个隐形表单提交的形式,后台文件流可以直接下载。无需其它过多的处理。


后台逻辑:生成PDF,并下载

service部分代码如下(示例):

import com.itextpdf.text.Document;
import com.itextpdf.text.Element;
import com.itextpdf.text.PageSize;
import com.itextpdf.text.Paragraph;

	@Override
    public void exportForm(HttpServletResponse response, String picBase64Info)
    {
        // 1.创建文档,设置文档页面大小,页边距
        Document document = new Document(PageSize.A4, Float.parseFloat("0"), Float.parseFloat("5"),
                Float.parseFloat("34"), Float.parseFloat("50"))

        // 2.解析前台传来的echart图,生成pdf段落元素
        Paragraph pictureEle = PDFUtil.createImageFromEncodeBase64(picBase64Info, "1、段落名称1", Float.parseFloat("34"),
                Float.parseFloat("50"), false);

        // 生成table表格段落(略去此处)
        // Paragraph table1 = PDFUtil.createTable(xx略, "2、段落名称2", new String[]
        { "数据a1", "数据a2", "数据a3" }, xx略);
        // Paragraph table2 = PDFUtil.createTable(xx略, "3、段落名称3", xx略, xx略);
        // Paragraph table3  = PDFUtil.createTable(xx略,"4、段落名称4", xx略, xx略);
        
        // 将所有需要放到文档里的元素,汇总
        List<Paragraph> paragraphs = new ArrayList<Paragraph>();
        paragraphs.add(pictureEle);
        //paragraphs.add(table1);
        //paragraphs.add(table2);
        //paragraphs.add(table3);

        // 导出pdf文档:将paragraphs塞到document中,并下载document
        PDFUtil.exportDocument(document, null, paragraphs, response, "报表");
    }

PDFUtil 工具类如下:

import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import com.itextpdf.text.BaseColor;
import com.itextpdf.text.Chunk;
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Element;
import com.itextpdf.text.Font;
import com.itextpdf.text.Image;
import com.itextpdf.text.Paragraph;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.PdfPCell;
import com.itextpdf.text.pdf.PdfPTable;
import com.itextpdf.text.pdf.PdfWriter;

/**
 * ClassName : PDFUtil <br/>
 * Function : (生成PDF,工具类) <br/>
 * date : 2022年8月1日 上午9:42:11 <br/>
 *
 * @author
 * @version 
 * @since JDK 1.8
 */
public class PDFUtil
{
    private static final Logger logger = LogManager.getLogger(PDFUtil.class);

    /**
     * fontSize_normal : (正文字体大小11号).
     */
    public static final float FONTSIZE_NORMAL = 11f;
    /**
     * fontSize_titile : (标题字体大小14号).
     */
    public static final float FONTSIZE_TITILE = 14f;
    /**
     * FONTSIZE_COVER : (封面字体大小32号).
     */
    public static final float FONTSIZE_COVER = 32f;

    /**
     * normalFont : (通用字体样式:宋体、11号).
     */
    private static Font normalFont = null;
    /**
     * titleFont : (通用标题字体样式:宋体、14号、加粗).
     */
    private static Font titleFont = null;
    /**
     * coverFont : (通用封面字体样式:宋体、28号、加粗).
     */
    private static Font coverFont = null;
    

    /**
     * getBaseFont : (获取可以解析中文的字体:使用宋体). <br/>
     *
     * @author 
     * @return
     * @since JDK 1.8
     */
    public static BaseFont getBaseFontChinese()
    {
        try
        {
            // 宋体资源文件路径,可以从C://Windows//Fonts//simsun.ttc拷贝到相应目录下
            URL path = PDFUtil.class.getResource("/config/fonts/simsun.ttc");
            return BaseFont.createFont(path + ",0", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);

            // 本地测试:使用windows自带的宋体文件
            // return BaseFont.createFont("C://Windows//Fonts//simsun.ttc,0", BaseFont.IDENTITY_H, false);
        }
        catch (Exception e)
        {
            logger.error("设置字体样式失败", e);
            return null;
        }
    }

    /**
     * getNormalFont : (获取普通字体样式). <br/>
     *
     * @author 
     * @return
     * @since JDK 1.8
     */
    public static Font getNormalFont()
    {
        if (normalFont == null)
        {
            BaseFont bfChinese = getBaseFontChinese();
            normalFont = new Font(bfChinese, FONTSIZE_NORMAL, Font.NORMAL);
        }
        return normalFont;
    }

    /**
     * getTitleFont : (获取标题通用字体). <br/>
     *
     * @author 
     * @return
     * @since JDK 1.8
     */
    public static Font getTitleFont()
    {
        if (titleFont == null)
        {
            BaseFont bfChinese = getBaseFontChinese();
            titleFont = new Font(bfChinese, FONTSIZE_TITILE, Font.BOLD);
        }
        return titleFont;
    }

    /**
     * getTitleFont : (获取封面通用字体). <br/>
     *
     * @author 
     * @return
     * @since JDK 1.8
     */
    public static Font getCoverFontFont()
    {
        if (coverFont == null)
        {
            BaseFont bfChinese = getBaseFontChinese();
            coverFont = new Font(bfChinese, FONTSIZE_COVER, Font.BOLD);
        }
        return coverFont;
    }

    /**
     * getfieldValue : (通过反射,根据方法名,执行方法,最终返回行结果的toString值). <br/>
     *
     * @author 
     * @param <T> 方法执行的入参
     * @param object 对象
     * @param methodName 方法名
     * @param args 方法执行参数
     * @since JDK 1.8
     */
    private static <T> String getfieldValue(T object, String methodName, Object... args)
    {
        try
        {
            Method method = object.getClass().getMethod(methodName);
            Object value = method.invoke(object, args);
            return value == null ? "" : value.toString();

        }
        catch (Exception e)
        {
            logger.error("getfieldValue error", e);
            return "";
        }
    }

    /**
     * analysisPicBase64Info : (解析base64图片信息). <br/>
     *
     * @author 
     * @param picBase64Info 图片base64信息,或前台echart通过调用getDataURL()方法获取的图片信息
     * @return 图片经过base64解码后的信息
     * @since JDK 1.8
     */
    public static Element analysisPicBase64Info(String picBase64Info)
    {
        if (StringUtils.isEmpty(picBase64Info))
        {
            // 空段落
            return new Paragraph();
        }

        // 1.获取图片base64字符串信息:若入参是通过前台echarts调用getDataURL()方法获取的,则该字符串包含逗号,且则逗号后面的内容才是图片的信息
        String pictureInfoStr = picBase64Info.indexOf(",") == -1 ? picBase64Info : picBase64Info.split(",")[1];
        // 2.将图片信息进行base64解密
        byte[] imgByte = Base64.decodeBase64(pictureInfoStr);

        // 对异常的数据进行处理
        /**
         * .图片的原始表达ascii码范围是0-255,
         * .这里面有一些不可见的编码。然后为了图片正确传输才转成编码base64的0-63,
         * .当从base64转成byte时,byte的范围是[-128,127],那么此时就会可能产生负数,而负数不是在ascii的范围里,所以需要转换一下
         */
        for (int i = 0; i < imgByte.length; i++)
        {
            if (imgByte[i] < 0)
            {
                imgByte[i] += 256;
            }
        }

        try
        {
            return Image.getInstance(imgByte);
        }
        catch (Exception e)
        {
            logger.error("analysisPicBase64Info error", e);
            return new Paragraph();
        }
    }

    /**
     * analysisPicBase64Info_batch : (批量解析base64加密的图片信息,生成Image对象). <br/>
     *
     * @author 
     * @param picBase64Infos 经过base64加密的图片信息
     * @return
     * @since JDK 1.8
     */
    public static List<Element> analysisPicBase64Info_batch(List<String> picBase64Infos)
    {
        List<Element> images = new ArrayList<Element>();
        for (String li : picBase64Infos)
        {
            Element image = analysisPicBase64Info(li);
            images.add(image);
        }
        return images;
    }

    /**
     * createImage : (根据图片的base64加密文件创建pdf图片). <br/>
     *
     * @author 
     * @param picBase64Info base64加密后的图片信息(支持台echart通过调用getDataURL()方法获取的图片信息)
     * @param title 段落标题
     * @param percentX 图片缩放比例X轴
     * @param percentY 图片缩放比例Y轴
     * @param titleCenter 标题是否居中,true-居中、false-默认居左
     * @return 返回图片段落
     * @since JDK 1.8
     */
    public static Paragraph createImageFromEncodeBase64(String picBase64Info, String title, float percentX,
            float percentY, boolean titleCenter)
    {
        // 1.获取图片
        Element element = analysisPicBase64Info(picBase64Info);
        // 2.创建段落,并添加标题
        Paragraph paragraph = new Paragraph(title, getTitleFont());
        // 空行
        paragraph.add(Chunk.NEWLINE);
        paragraph.add(Chunk.NEWLINE);

        if (!(element instanceof Image))
        {
            // 图片解析失败
            return paragraph;
        }

        Image image = (Image) element;
        // 3.设置图片缩放比例
        image.scalePercent(percentX, percentY);

        // 4.图片放入该段落
        paragraph.add(image);

        return paragraph;
    }

    /**
     * createImageFromEncodeBase64_batch : (批量创建). <br/>
     *
     * @author 
     * @param picBase64Infos 图片base64加密后的信息(支持台echart通过调用getDataURL()方法获取的图片信息)
     * @param titles 段落标题
     * @param percentXs X轴缩放比例
     * @param percentYs Y轴缩放比例
     * @param titleCenter 标题是否居中
     * @return
     * @since JDK 1.8
     */
    public static Paragraph createImageFromEncodeBase64_batch(List<String> picBase64Infos, List<String> titles,
            List<Float> percentXs, List<Float> percentYs)
    {
        Paragraph paragraphs = new Paragraph();
        for (int i = 0; i <= picBase64Infos.size(); i++)
        {
            Paragraph imagePara = createImageFromEncodeBase64(picBase64Infos.get(i), titles.get(i), percentXs.get(i),
                    percentYs.get(i), titleCenter);
            if (!imagePara.isEmpty())
            {
                paragraphs.add(imagePara);
                // 空行
                paragraphs.add(Chunk.NEWLINE);
                paragraphs.add(Chunk.NEWLINE);
            }
        }
        return paragraphs;
    }

    /**
     * createTable : (创建table段落). <br/>
     *
     * @author 
     * @param <T>
     * @param list 构建table的数据
     * @param title 该段落取的名字
     * @param methodNames 需要调用的方法名,用来获取单元格数据。通常是某个属性的get方法
     * @return
     * @since JDK 1.8
     */
    public static <T> Paragraph createTable(List<T> list, String title, String[] tableHead, List<String> methodNames)
    {
        return createTable(list, FONTSIZE_NORMAL, FONTSIZE_TITILE, title, tableHead, methodNames, false);
    }

    /**
     * createTable : (创建table段落). <br/>
     *
     * @author 
     * @param <T>
     * @param list 构建table的数据
     * @param title 该段落取的名字
     * @param methodNames 需要调用的方法名,用来获取单元格数据。通常是某个属性的get方法
     * @param titleCenter 标题是否居中:true-居中,false-居左
     * @return
     * @since JDK 1.8
     */
    public static <T> Paragraph createTable(List<T> list, String title, String[] tableHead, List<String> methodNames)
    {
        return createTable(list, FONTSIZE_NORMAL, FONTSIZE_TITILE, title, tableHead, methodNames);
    }

    /**
     * createTableByList : (创建table段落). <br/>
     *
     * @author 
     * @param <T>
     * @param listData 
     * @param normalFontSize 正文字体大小
     * @param titleFontSize 标题字体大小
     * @param title 段落名称
     * @param methodNames 获取表格属性的方法名
     * @param titleCenter 段落标题是否水平居中,true:需要居中;false:默认靠左
     * @return
     * @since JDK 1.8
     */
    public static <T> Paragraph createTable(List<T> listData, float normalFontSize, float titleFontSize,
            String title, String[] tableHead, List<String> methodNames)
    {
        // 1.创建一个段落
        Paragraph paragraph = new Paragraph(title, getTitleFont());
        // 空行
        paragraph.add(Chunk.NEWLINE);
        paragraph.add(Chunk.NEWLINE);

        // 3.创建一个表格
        PdfPTable table = new PdfPTable(methodNames.size());// 列数
        paragraph.add(table);

        // 4.构造表头
        for (String head : tableHead)
        {
            head = StringUtils.isEmpty(head) ? "" : head;
            PdfPCell cell = new PdfPCell(new Paragraph(head, getNormalFont()));
            cell.setBackgroundColor(
                    new BaseColor(Integer.parseInt("124"), Integer.parseInt("185"), Integer.parseInt("252")));// 背景色
            cell.setMinimumHeight(Float.parseFloat("15"));// 单元格最小高度
            cell.setHorizontalAlignment(Element.ALIGN_CENTER);// 水平居中
            table.addCell(cell);
        }

        if (CollectionUtils.isEmpty(listData))
        {
            // 没有数据,添加一行空单元格,并返回
            for (int i = 0; i < methodNames.size(); i++)
            {
                table.addCell(new Paragraph(" "));// 有一个空格,否则添加不了
            }
            return paragraph;
        }

        // 5.构造table数据
        for (T li : listData)
        {
            for(String name : methodNames)
            {
                String valueStr = getfieldValue(li, name);
                PdfPCell cell = new PdfPCell(new Paragraph(valueStr, getNormalFont()));
                cell.setHorizontalAlignment(Element.ALIGN_CENTER);// 水平居中
                table.addCell(cell);
            }
        }
        // 5.返回
        return paragraph;
    }

    /**
     * addToTable : (从段落中找到第一个table,向该table中追加数据). <br/>
     * (). <br/>
     *
     * @author 
     * @param <T>
     * @param paragraph
     * @param listData
     * @param methodNames
     * @since JDK 1.8
     */
    public static <T> void addToTable(Paragraph paragraph, List<T> listData, List<String> methodNames)
    {
        for (Element ele : paragraph)
        {
            if (!(ele instanceof PdfPTable))
            {
                // 不是table元素,直接跳过
                continue;
            }

            // 找到第一个table元素
            PdfPTable table = (PdfPTable) ele;
            for (T data : listData)
            {
                for (String name : methodNames)
                {
                    String valueStr = getfieldValue(data, name);
                    PdfPCell cell = new PdfPCell(new Paragraph(valueStr, getNormalFont()));
                    cell.setHorizontalAlignment(Element.ALIGN_CENTER);// 水平居中
                    table.addCell(cell);
                }
            }
            break;
        }

    }

    /**
     * exportDocument : (生成并下载PDF文档). <br/>
     * (). <br/>
     *
     * @author 
     * @param document 文档对象
     * @param cover 封面:若不是null,则会先添加封面,并另起新页面添加段落
     * @param paragraphs 需要组成PDF文件的段落
     * @param response 请求的响应对象
     * @param fileName 生成的文件名称,不需要加pdf后缀
     * @since JDK 1.8
     */
    public static void exportDocument(Document document, Paragraph cover, List<Paragraph> paragraphs,
            HttpServletResponse response, String fileName)
    {
        try (ServletOutputStream out = response.getOutputStream())
        {
            response.setContentType("application/binary;charset=UTF-8");
            response.setHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(fileName + ".pdf", "UTF-8"));

            PdfWriter.getInstance(document, out);
            // 打开文档
            document.open();

            if (cover != null)
            {
                document.add(cover);
                // 起新页面
                document.newPage();
            }

            StringBuilder errorMsg = new StringBuilder();
            for (int i = 0; i < paragraphs.size(); i++)
            {
                try
                {
                    // 将段落添加到文档
                    document.add(paragraphs.get(i));
                    // 空行
                    document.add(Chunk.NEWLINE);
                    document.add(Chunk.NEWLINE);
                }
                catch (DocumentException e)
                {
                    errorMsg.append("PDF文件生成出错,请检查第:").append(i).append("个段落");
                }
            }

            if (!StringUtils.isEmpty(errorMsg.toString()))
            {
                logger.error(errorMsg);
            }

            // 关闭文档
            document.close();
            out.flush();
            out.close();
        }
        catch (Exception e)
        {
            logger.error("生成PDF文档并下载,出错:", e);
        }

    }

    /**
     * setDefaultIndentationLeft : (设置段落默认左边距). <br/>
     *
     * @author 
     * @param paragraph
     * @param indentation
     * @since JDK 1.8
     */
    public static void setDefaultIndentationLeft(Paragraph paragraph)
    {
        paragraph.setIndentationLeft(Float.parseFloat("30"));
    }

    /**
     * addBlankLine : (添加空行). <br/>
     *
     * @author 
     * @param paragraph 需要添加空行的段落
     * @param lineNum 需要添加空行的个数
     * @since JDK 1.8
     */
    public static void addBlankLine(Paragraph paragraph, int lineNum)
    {
        if (paragraph == null)
        {
            return;
        }

        for (int i = 0; i < lineNum; i++)
        {
            paragraph.add(Chunk.NEWLINE);
        }
    }
}

PDF导出效果

在这里插入图片描述


总结

以上就是今天要讲的内容,简单介绍了JAVA如何创建PDF、如何将一张图片写入PDF、如何在前台拿到Echarts的图表。工具方法中提到了如何在PDF中创建表格。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值