springboot + Velocity引擎,生成PDF文件

Java 生成PDF模版很火的一个方案就是先生成html,再转化成PDF文件
优点:有现成的成熟框架,只需要专注html模版生成自己想要的格式

缺点:如果很多场景下需要生成PDF文件,那就要定义很多个html模版,不好维护,不好扩展
但是,,,,,小子有一个不成熟的方案,请各位大佬指点

思路:
就像生成一个excel模版一样,我们只需要一行一列的填充数据,根本不需要关心模版,或者说根本就没有模版!
那其实PDF文件,不也是一行一列的数据吗,我们只需要用一套html模版,把所有数据都动态渲染不就可以了吗?

代码思路:
1、定义一个html模版
2、再定义一个和html融合的实体类(标注着数据的位置、格式等);
3、融合的实体类赋值
4、最终html文件就由这两者来生成
5、html转化为PDF 完结

代码

所支持的PDF内容有 (普通文本,图片,表格)先贴一下最终目标图片

html 模版

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
</head>
<body>
<div>

    #foreach($item in $pdfTemplateDO.properties)
        #if($!item.valueType == 2)
            <div width="100%" style="display: flex;flex-direction: column;align-items: center;">
                <img src=$!{item.value} alt="图片地址错误"  width="$!{item.imgWidth}px" height="$!{item.imgHeight}px">
            </div>
        #else
            <div class="textSet">
                #if($!item.displayName != 'null')
                $!{item.displayName}:$!{item.displayNameBlank}

                #else
                     <p><p/>
                #end

                #if($!item.value != 'null')
                   $!{item.value}
                #else
                    <p><p/>
                #end

            </div>
        #end

    #end

    <br/>
    <br/>
    #if($!pdfTemplateDO.tableProperties.size()>0)
    <table border="1" cellSpacing="0"  width="100%">
        #foreach($tableProperty in $pdfTemplateDO.tableProperties)
        <tr>
            #foreach($item in $tableProperty)
                <td>
                    #if($!item=='null')
                        <p></p>
                    #else
                        <p> $!{item}</p>
                  </td>
                #end
            #end
        </tr>
        #end
    </table>
    #end
</div>

</body>
<style>
    .textSet{
             width: auto;
             height: auto;
             font-size:20px;
             font-family:Times,serif;
             margin-bottom:10px;
    }
    table
    {
      width:100%;
        border-collapse:collapse;
        text-align: center;
    }
  </style>
</html>

 

Java 代码

/**
上述融合类
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class PdfTemplateDTO {
    /**
     * 单属性
     */
    private List<SingleProperty> properties;

    /**
     * 列属性(表头以及每一行数据)
     */
    private List<ColumnProperty> lineProperties;

    /**
     * 列属性转化格式(用于表格展示)
     */
    private List<List<String>> tableProperties = new ArrayList<>();

    public void linePropertiesConvertTable(){
        if (CollectionUtils.isEmpty(this.lineProperties)){
            this.tableProperties = new ArrayList<>();
            return;
        }

        List<String> collectHeadName = this.lineProperties.stream().map(v -> v.getTableHeadName()).collect(Collectors.toList());
        //表头
        tableProperties.add(collectHeadName);

        //表体(行列转化)
        for (int i = 0; i < this.lineProperties.get(0).getTableValue().size(); i++) {
            List<String> row = new ArrayList<>();
            for (int j = 0; j<this.lineProperties.size(); j++){

                //获取第j行数据的第i个数据
                String s = lineProperties.get(j).getTableValue().get(i);
                row.add(s);
            }
            tableProperties.add(row);
        }
    }
}



/**
  自定义注解
**/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface AssignGeneratePDF {
/**
* 模版中展示的名称
* @return
*/
String displayName() default "";

/**
* 模版中的顺序(可拓展xy支持一行展示多列)
* @return
*/
int sort() default 9999;

/**
* 属性类型
* 1、单属性
* 2、表格属性
*/
int propertyType() default 1;

/**
* 值类型
* 1、文本
* 2、图片
*/
int valueType() default 1;

int imgWidth() default 200;

int imgHeight() default 120;
}



/**
核心实现代码
**/
public class GeneratePDFUtil {

static String pdfTemplateDO = "pdfTemplateDO";
static String templateName = "PDFTemplate.html";
private static final String wms_basic_data = "wms_basic_data";
private static final String defaultFileName = System.currentTimeMillis()+".pdf";
private static final String MODEL = "model";
private static final String DATA = "data";

private static String space = "&ensp;";

private static Integer thirteen = 12;

static {
// Velocity初始化
Velocity.setProperty(RuntimeConstants.OUTPUT_ENCODING, StandardCharsets.UTF_8);
Velocity.setProperty(RuntimeConstants.INPUT_ENCODING, StandardCharsets.UTF_8);
Velocity.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
Velocity.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
Velocity.init();
}

public static void getPDFFileVO(Object dataSource, String fileName){
FileVO fileVO = new FileVO();
if (null == dataSource){
return fileVO;
}
Loggers.BIZ.warn(GeneratePDFUtil.class.getName(),"getPDFFileVO","获取PDF文件","数据源信息",JSONUtil.toJsonStr(dataSource),null);
ByteArrayOutputStream byteArrayOutputStream = generatePDF(dataSource);

}

public static ByteArrayOutputStream generatePDF(Object dataSource){
try {
PdfTemplateDTO pdfTemplateDTO = getFullTemplateDate(dataSource);
//融合数据到html的容器
VelocityContext context = new VelocityContext();
context.put(pdfTemplateDO, pdfTemplateDTO);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
pdfFile(context, templateName, outputStream);
return outputStream;
} catch (IllegalAccessException e) {
throw new ApplicationException("pdf 文件字节流生成失败"+e.getMessage());
}
}

/**
* 根据数据源生成模版
* 注意属性类型建议设置成String
* @param dataSource
* @return
* @throws IllegalAccessException
*/
public static PdfTemplateDTO getFullTemplateDate(Object dataSource) throws IllegalAccessException {
PdfTemplateDTO pdfTemplateDO = new PdfTemplateDTO();
if (dataSource == null){
return pdfTemplateDO;
}
//单属性初始化
ArrayList<SingleProperty> propertyDOS = new ArrayList<>();
//按照表头对应此列的格式先收集数据(为列排序)
HashMap<String, ColumnProperty> mapCollect = new HashMap<>();

Class<?> aClass = dataSource.getClass();
Field[] fields = aClass.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
if (!field.isAnnotationPresent(AssignGeneratePDF.class)){
continue;
}
AssignGeneratePDF annotation = field.getAnnotation(AssignGeneratePDF.class);
//单属性
if (annotation.propertyType() == 1){
SingleProperty singleProperty = new SingleProperty();
singleProperty.setDisplayName(annotation.displayName());
singleProperty.setSort(annotation.sort());
String v = toJSONString(field.get(dataSource));
singleProperty.setValue(v);
singleProperty.setValueType(annotation.valueType());
singleProperty.setImgWidth(annotation.imgWidth());
singleProperty.setImgHeight(annotation.imgHeight());
propertyDOS.add(singleProperty);
}else if (annotation.propertyType() == 2){
//列属性
Object o = field.get(dataSource);
if(CollectionUtils.isEmpty((List)o)){
continue;
}
if (!(o instanceof List)){
throw new RuntimeException("AssignGeneratePDF 自定义注解使用失败,type=2不可修饰非List属性");
}
List data = (List)o;
if (CollectionUtils.isEmpty(data)){
continue;
}
//循环表格数据
for (Object datum : data) {
Class<?> aClassSub = datum.getClass();
Field[] fieldListSub = aClassSub.getDeclaredFields();
for (Field fieldSub : fieldListSub){
fieldSub.setAccessible(true);
if (!fieldSub.isAnnotationPresent(AssignGeneratePDF.class)){
continue;
}
AssignGeneratePDF annotationSub = fieldSub.getAnnotation(AssignGeneratePDF.class);
String displayName = annotationSub.displayName();
if (fieldSub.get(datum) instanceof List || annotationSub.propertyType() != 1) {
Loggers.BIZ.warn(GeneratePDFUtil.class.getName(),"getFullTemplateDate",String.format("数据源:%s", JSONUtil.toJsonStr(dataSource)),
"AssignGeneratePDF 表格属性内字段propertyType只能为1,不可为集合",null);
continue;
}
String v = toJSONString(fieldSub.get(datum));
ColumnProperty columnPropertyDO = mapCollect.get(displayName);
if (columnPropertyDO == null){
columnPropertyDO = new ColumnProperty();
columnPropertyDO.setSort(annotationSub.sort());
columnPropertyDO.setTableHeadName(displayName);
List<String> tableValue = new ArrayList<>();
columnPropertyDO.setTableValue(tableValue);
tableValue.add(v);
mapCollect.put(displayName,columnPropertyDO);
}else {
columnPropertyDO.getTableValue().add(v);
}
}
}

}
}
//空格适配
spaceAaptation(propertyDOS);
//排序
List<SingleProperty> finalProperties = propertyDOS.stream().sorted(Comparator.comparing(SingleProperty::getSort)).collect(Collectors.toList());
List<ColumnProperty> finalLineValues = mapCollect.values().stream().sorted(Comparator.comparing(ColumnProperty::getSort)).collect(Collectors.toList());
pdfTemplateDO.setProperties(finalProperties);
pdfTemplateDO.setLineProperties(finalLineValues);
pdfTemplateDO.linePropertiesConvertTable();
return pdfTemplateDO;
}

/**
* displayName和值之间的距离
* @param properties
*/
private static void spaceAaptation(List<SingleProperty> properties){
double max = properties.stream()
.filter(v -> v.getDisplayName() != null)
.map(v -> getStringLength(v.getDisplayName()))
.max(Comparator.comparing(v -> v)).orElse(1d);

//最大个数上+1个空格 作为标长
for (SingleProperty property : properties) {
if(property.getDisplayName() == null){
continue;
}
double spaceNumber = max - getStringLength(property.getDisplayName());
StringBuilder stringBuilder = new StringBuilder();
for (Integer i = 0; i < spaceNumber*2; i++) {
stringBuilder.append(space);
}
property.setDisplayNameBlank(stringBuilder.toString());
}
}

/**
* 英文算一半
* @return
*/
private static double getStringLength(String str){
if (str == null){
return 0L;
}
double valueLength = 0;
String chinese = "[\u4e00-\u9fa5]";
for (int i = 0; i <str.length(); i++) {
// 获取一个字符
String temp = str.substring(i, i + 1);
// 判断是否为中文字符
if (temp.matches(chinese)) {
// 中文字符长度为1
valueLength += 1;
} else {
// 其他字符长度为0.5
valueLength += 0.5;
}
}
return valueLength;
}


public static String toJSONString(Object o) {
String result = null;
if (o == null) {
return result;
} else if (o instanceof String) {
result = (String)o;
} else if (o instanceof Number) {
result = o.toString();
} else if (o instanceof Boolean) {
result = o.toString();
} else if (o instanceof Date) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
result = dateFormat.format(o);
} else {
throw new IllegalArgumentException("pdf生成 not support data type : " + o.getClass());
}

return result;
}


/**
* 据模板生成pfd格式文件
*
* @param context 上下文对象
* @param template pdf模板
* @param outputStream 生成ofd文件输出流
*/
public static void pdfFile(Context context, String template, OutputStream outputStream) {
try (PdfWriter pdfWriter = new PdfWriter(outputStream)) {
PdfDocument pdfDocument = new PdfDocument(pdfWriter);
pdfDocument.setDefaultPageSize(PageSize.A4);

ConverterProperties properties = new ConverterProperties();
FontProvider fontProvider = new FontProvider();
// 字体设置,解决中文不显示问题
PdfFont sysFont = PdfFontFactory.createFont("STSongStd-Light", "UniGB-UCS2-H");
fontProvider.addFont(sysFont.getFontProgram(), "UniGB-UCS2-H");
properties.setFontProvider(fontProvider);

Template pfdTemplate = Velocity.getTemplate(template, "UTF-8");
StringWriter writer = new StringWriter();
//模版 模版数据填充
pfdTemplate.merge(context, writer);
HtmlConverter.convertToPdf(writer.toString(), pdfDocument, properties);
pdfDocument.close();
} catch (Exception e) {
throw new RuntimeException("PFD文件生成失败", e);
}
}
}


测试java 代码

/**测试实体类
**/
@Data
public class ShangJiaDan{

@AssignGeneratePDF(displayName = "ASN单号",sort = 1)
private String asnNo;

@AssignGeneratePDF(displayName = "操作人姓名",sort = 2)
private String operationUserName;

@AssignGeneratePDF(displayName = "姓名",sort = 3)
private String userName;

@AssignGeneratePDF(displayName = "日期",sort = 4)
private Date date;

@AssignGeneratePDF(propertyType = 2)
private List<ShangJiaDan> tableDate;

@AssignGeneratePDF(valueType = 2,sort = 5)
private String imageUrl;
}



@SpringBootTest @RunWith(SpringRunner.class) public class ExportService { @Test public void testPdf(){ ShangJiaDan shangJiaDan = new ShangJiaDan(); shangJiaDan.setUserName("陈那位i吃为我"); shangJiaDan.setAsnNo(" nvousi2382"); shangJiaDan.setOperationUserName("小冰i好"); shangJiaDan.setDate(new Date()); shangJiaDan.setImageUrl("https://x0.ifengimg.com/res/2021/5EAA10AC691D371504428B1E8C9CA0B5A632A344_size1548_w1150_h638.png"); ShangJiaDan shangJiaDan1 = new ShangJiaDan(); shangJiaDan1.setUserName("第二位"); shangJiaDan1.setAsnNo(" 02222"); shangJiaDan1.setDate(new Date()); ShangJiaDan shangJiaDan2 = new ShangJiaDan(); shangJiaDan2.setUserName("第是你位"); shangJiaDan2.setOperationUserName("奥力给"); shangJiaDan2.setAsnNo(" 02222"); shangJiaDan2.setDate(new Date()); shangJiaDan1.setImageUrl("3232.roepe.png"); shangJiaDan.setTableDate(Arrays.asList(shangJiaDan1,shangJiaDan2,shangJiaDan1)); FileVO pdfFileVO = GeneratePDFUtil.getPDFFileVO(shangJiaDan, "1.pdf"); String fileUrl = pdfFileVO.getFileUrl(); System.out.println(fileUrl); } }

 

测试结果

 

posted @ 2023-06-25 19:15  bkytiantao  阅读(1197)  评论(0)    收藏  举报