PDF生成目录
- 前言
- 功能
- 1. 使用前端抓取打印
- 2. 使用后端PDF模板生成
- 2.1 pdf模板的概念
- 2.2 下载链接
- 2.3 使用方法
- 1. 首先创建好我们的基础模板
- 2. 打开后就是我们的创建表单域的界面
- 3. 创建基本的文本域
- 4. 编写后端代码
- 5. 总结
- 3. 使用后端itext动态绘制模板
- 问题汇总
- 1.1 为什么前端不能使用多图打印及数据量的情况
- 1.2 什么时候适合使用前端打印,什么时候适合后端模板呢
- 3.1 PDF文件中中文无法正常显示
- 3.2 想使用自定义字体怎么办?
- 3.3 怎样给PDF中添加水印功能
- 3.4 如何给PDF表格中批量添加图片?
- 3.5 动态生成的PDF如何实现分页?
- 3.6 评论的多图片处理思路,及缩小处理时间
前言
本文用于介绍实现的方案,具体的代码层次只是大概的去涉及些,并提供开发过程中需要下载的aodbe DC工具及字体文件等,目的是为了有一个PDF的概念,方便后面开发功能能够知道怎么开发如何如去开发持续去完善,期间产生的问题,有些记不清了,等后面使用过程中慢慢去补充,也算是一些经验记录.也欢迎一块讨论一块补充,找出文中存在的Bug
这里再抛出个问题,目前尚不知道如何去实现.批量PDF中的批量图片怎样短时间内导出呢?
功能
在业务系统中生成PDF文件有三种方法,分为两个层次,一个前端打印, 一个后端打印,其中后端打印又分为两种,一种是使用模板打印,另外一种使用动态生成PDF文件
1. 使用前端抓取打印
需要打印的节点,调用方法即可使用,使用于单页或者图片数据量渲染少的情况 否则会产生问题. 见问题汇总[1.1]
windows.print();
默认print打印的是根节点,如果需要打印自定义节点就需要自己去抓取,把不想打印的地方临时remove掉或者是指定节点,这个具体再百度
2. 使用后端PDF模板生成
2.1 pdf模板的概念
后端PDF模板原理是通过提前准备好一个pdf模板,布局好pdf模板的样式,使用Adobe Acrobat Reader DC 去对模板进行表单域处理 预填好对应的属性 ,诸如下图中被红框所圈起来的地方就是表单域,这里统一使用的是文本域 左下角四个图片展示的地方使用的是图片域这里提供下思路,对这个pdf模板进行解析下,其中的黑体文字因为是死值,所以提前在模板中就把这些值写死了,而灰色框中的文本是为了预览显示的默认值,等这里填充字段了,就会被修改.我们所需要做的就是组织好pdf的样式,然后把数据填充进去即可,想起来其实是跟前端打印一个道理,最终还是要组织好样板去打印生成
2.2 下载链接
免费版DC下载 好吧,更正下,阿里云不能下载Zip,所以这个失效了…,得自己找了
下载完成后直接点击安装即可
2.3 使用方法
这里做个大概的示范,具体的使用及出现的问题还是需要自己去排查摸索
1. 首先创建好我们的基础模板
来源可以是自己使用DC做的或者是其他方面提供的 点击签名->创建表单->在下面选择我们准备好的模板
2. 打开后就是我们的创建表单域的界面
红框所示的就是我们的工具栏,这块的操作需要慢慢摸索
3. 创建基本的文本域
这里填写的就是我们后端所需要的的属性名
举例: 这里填写 name
后端: map.put("name","张三");
4. 编写后端代码
<!-- pom文件 itext 依赖 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.4.3</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>org.apache.poi.xwpf.converter.pdf</artifactId>
<version>1.0.4</version>
</dependency>
// pdf txt 测试
OutputStream os = null;
ByteArrayOutputStream baos = null;
String template = "E:/test/ceshi.pdf"; //模板PDF的位置
Map<String, Object> treeDto = new HashMap<>(); // 构建模板所需要的文本数据
treeDto.put("titile", "标题");
treeDto.put("name", "姓名");
treeDto.put("sex", "性别");
treeDto.put("idCard", "用户身份证号");
/*
* 构建模板中所需要的的图片 这里value都是图片的存储路径
* 但是最终都需要解析为流所以传参什么需要自己判断
* */
treeDto.put("stuImg","图片地址1");
treeDto.put("img1", "图片地址2");
treeDto.put("img2", "图片地址3");
treeDto.put("img3", "图片地址4");
try {
os = response.getOutputStream();
//读取模板资源文件
PdfReader reader = new PdfReader(template);
baos = new ByteArrayOutputStream();
//获取操作对象
PdfStamper stamper = new PdfStamper(reader, baos);
AcroFields form = stamper.getAcroFields();
//拿到pdf所存在的表单域
Iterator<String> it = form.getFields().keySet().iterator();
while (it.hasNext()) {
// 这里就是pdf中我们编写的文本域的名称,如果相等则匹配 赋予相对应的值
String name = it.next().toString();
//写入图片
if ("stuImg".equals(name) || "img1".equals(name) || "img2".equals(name) || "img3".equals(name)) {
Image image = Image.getInstance(treeDto.get(name).toString());
form.addSubstitutionFont(BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED));
//设置图片应该显示的位置 这里因为只有一张pdf 所以直接get(0)
int pageNo = form.getFieldPositions(name).get(0).page;
// 拿到pdf所对应的表单域中的界限 x y
Rectangle signRect = form.getFieldPositions(name).get(0).position;
float x = signRect.getLeft();
float y = signRect.getBottom();
PdfContentByte under = stamper.getOverContent(pageNo);
image.scaleToFit(signRect.getWidth(), signRect.getHeight());
//控制图片
image.setAbsolutePosition(x, y);
//添加图片到pdf中
under.addImage(image);
//写入文字
} else {
form.setField(name, treeDto.get(name).toString());
}
}
stamper.setFormFlattening(true);
stamper.close();
Document doc = new Document();
PdfCopy copy = new PdfCopy(doc, os);
doc.open();
PdfImportedPage importPage = copy.getImportedPage(new PdfReader(baos.toByteArray()), 1);
copy.addPage(importPage);
doc.close();
// response.reset(); 这里本意是清空管道,但是存在一些问题
//以流的形式返回给浏览器 这样一般的浏览器就用自带的工具打开pdf或者下载 特别实用
logger.info("开始返回PDF");
//设置为pdf格式
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "attachment;fileName="
+ URLEncoder.encode(fileName, "UTF-8"));
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
logger.info("生成预览文件成功");
} catch (Exception e) {
logger.info("返回PDF失败" + e.getMessage());
e.fillInStackTrace();
} finally {
// 不管成功与否关闭流
IOUtils.closeQuietly(os);
}
5. 总结
只要能拿到模板对应的对象,我们就可以操作对象对文本等其他属性进行赋值,只要能想到的基本都能
够去操作.功能点很是很多的,不过基本上80%问题使用这些在模板的固定+数据的赋值+代码的配置上
都能解决了模板功能及代码编写上都能有很多其他的方法去实现更简洁更实用符合自己业务的功能,
这个就需要后续慢慢去摸索了.
3. 使用后端itext动态绘制模板
没错,这里又是使用itext来生成pdf文件,不过和模板最大的不同是模板只能在固定死的模板上进行缝缝补补的操作
,而这个功能可以实现动态的实现pdf,真正意义上的通过代码生成pdf文件
<!-- itextpdf 注意itext 的组件版本保持一致-->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13.1</version>
</dependency>
<!-- itext-asian 语言包 可以独立出来-->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<!-- itextpdf-tool-xmlworker -->
<dependency>
<groupId>com.itextpdf.tool</groupId>
<artifactId>xmlworker</artifactId>
<version>5.5.13.1</version>
</dependency>
要使用动态生成pdf文件功能首先要了解几个概念
- 一个PDF 是以Document 为对象 操作的是这个对象里面的属性,可以把document对象想象成打印的一张纸,如果需要多页pdf 那就把内容向下填充
- 前期使用会出现一堆问题,如字体 及中文支持上见问题3.1~3.2
- 使用pdf创建表格的时候要有一个列的概念,这点在poi excel中也是,在脑袋中表格就是由n行n列的单元格组成,要保证整体是一个长方形
- 实现过程是从上到下,从左到右,操作document节点
- pdf填充内容的过程是先打开文件,打开执行写入程序后,才会关闭
工具类:
package com.smart.safety.util;
import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* @author MinChang
* @Date 2021/8/2
*/
public class PdfUtil {
public static BaseFont bfChinese;
private static final int defaultSize =12;
public static BaseFont kaiTiFont;
static {
try {
//指定字体 没有这个则 createChineseFont 方法无用
bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H",
BaseFont.NOT_EMBEDDED);
} catch (DocumentException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
static {
try {
//指定字体 没有这个则 createChineseFont 方法无用
kaiTiFont = BaseFont.createFont( "/ttf/KAITIGB2312.ttf",BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
} catch (DocumentException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* @param table 表格
* @param border 边框
* @param rightIndent 占比
* @param text 文字
* @param size size
* @return
*/
public static void createCell(PdfPTable table,int border,float rightIndent,String text,int size,int rowSpan,int colSpan){
PdfPCell pdfPCell = new PdfPCell();
pdfPCell.setBorder(border); //设置边框
pdfPCell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
pdfPCell.setRightIndent(rightIndent);
pdfPCell.setPaddingTop(2f);//把字垂直居中
pdfPCell.setPaddingBottom(8f);//把字垂直居中
pdfPCell.setRowspan(rowSpan);
pdfPCell.setPhrase(PdfUtil.createChineseFont(text,size));
pdfPCell.setColspan(colSpan);
table.addCell(pdfPCell);
}
public static void createCell(PdfPTable table,String text,int size){
int border=Rectangle.BOX;
int rowSpan=1;
int colSpan=1;
float rightIndent=1;
PdfPCell pdfPCell = new PdfPCell();
pdfPCell.setBorder(border); //设置边框
pdfPCell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
pdfPCell.setRightIndent(rightIndent);
pdfPCell.setPaddingTop(2f);//把字垂直居中
pdfPCell.setPaddingBottom(8f);//把字垂直居中
pdfPCell.setRowspan(rowSpan);
pdfPCell.setPhrase(PdfUtil.createChineseFont(text,size));
pdfPCell.setColspan(colSpan);
table.addCell(pdfPCell);
}
public static void createCell(PdfPTable table,String text){
int size = 8;
int border=Rectangle.BOX;
int rowSpan=1;
int colSpan=1;
float rightIndent=1;
PdfPCell pdfPCell = new PdfPCell();
pdfPCell.setBorder(border); //设置边框
pdfPCell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
pdfPCell.setRightIndent(rightIndent);
pdfPCell.setPaddingTop(2f);//把字垂直居中
pdfPCell.setPaddingBottom(8f);//把字垂直居中
pdfPCell.setRowspan(rowSpan);
pdfPCell.setPhrase(PdfUtil.createChineseFont(text,size));
pdfPCell.setColspan(colSpan);
table.addCell(pdfPCell);
}
/**
* 返回中文Paragraph 默认12
* @param text
* @return
*/
public static Paragraph createChineseFont(String text){
Font c = new Font(bfChinese, defaultSize, Font.NORMAL);
return new Paragraph(text, c);
}
/**
* 返回中文Paragraph
* @param text
* @param size
* @return
*/
public static Paragraph createChineseFont(String text,int size){
if (size==0){
size=defaultSize;
}
Font c = new Font(bfChinese, size, Font.NORMAL);
return new Paragraph(text, c);
}
/*
* @Description 蓝色背景色标题内容行添加
* @Date 2019/7/12 14:56
* @param table 表格
* @param cell 列
* @param text 文本
* @return void
**/
public static void addTableGroupTitle(PdfPTable table, PdfPCell cell, String text) {
cell = new PdfPCell(new Phrase(text,getColorFont(BaseColor.WHITE)));
table.addCell(addTitleCell(cell,25,new BaseColor(69,153,241),2,false));
}
/**
* @Description 蓝色背景色标题内容行添加
* @Date 2019/7/12 14:56
* @param table 表格
* @param cell 列
* @param text 文本
* @param colspan 需要合并的列
* @return void
**/
public static void addTableGroupTitle(PdfPTable table, PdfPCell cell, String text,int colspan) {
cell = new PdfPCell(new Phrase(text,getColorFont(BaseColor.WHITE)));
table.addCell(addTitleCell(cell,25,new BaseColor(69,153,241),colspan,false));
}
/**
* @Description 核查建议
* @Date 2019/7/12 14:43
* @param table 表格
* @param cell 列
* @param suggestText 核查建议内容
* @param fontColor 核查建议内容文字颜色
* @return com.itextpdf.text.Element
**/
public static void addSuggestLine(PdfPTable table,PdfPCell cell,String suggestText,BaseColor fontColor) throws Exception {
addSuggestLine(table, cell, suggestText, fontColor, 0,10f,30f);
}
/**
* @Description 核查建议
* @Date 2019/7/12 14:43
* @param table 表格
* @param cell 列
* @param suggestText 核查建议内容
* @param fontColor 核查建议内容文字颜色
* @param colspan 合并的列
* @param widths 列所占宽
* @return com.itextpdf.text.Element
**/
public static void addSuggestLine(PdfPTable table,PdfPCell cell,String suggestText,BaseColor fontColor,int colspan,float...widths) throws Exception {
cell = new PdfPCell(new Phrase("核查建议:",getColorFont()));
cell.setColspan(1);
table.addCell(addBaseCell(cell,23,new BaseColor(238,238,238),false));
cell = new PdfPCell(new Phrase(suggestText,getColorFont(fontColor)));
if(colspan>0){
cell.setColspan(colspan);
}
table.addCell(addBaseCell(cell,23,new BaseColor(238,238,238),false));
table.setWidths(getColumnWiths(widths));
}
/**
* @Description 信息分组table
* @Date 2019/7/12 14:43
* @param groupText 文本内容
* @return com.itextpdf.text.Element
**/
public static Element addTableGroupLine(String groupText) {
PdfPTable tableBaseInfoIndex = new PdfPTable(1);
tableBaseInfoIndex.setWidthPercentage(20);
PdfPCell cellBaseInfo = new PdfPCell(new Phrase(groupText,getColorFont()));
cellBaseInfo.setHorizontalAlignment(Element.ALIGN_CENTER);
tableBaseInfoIndex.addCell(addTitleCell(cellBaseInfo,28,new BaseColor(238,238,238),2,false));
tableBaseInfoIndex.addCell(addBlankLine(10,1));
return tableBaseInfoIndex;
}
/**
* @Description 指定颜色字体 默认处理中文显示
* @Date 2019/7/12 14:05
* @param color 字体颜色
* @param fontSize 字体大小
* @param fontFamily 字体
* @return com.itextpdf.text.Font
**/
public static Font getColorFont(BaseColor color, int fontSize, String fontFamily) {
Font font = new Font(getFont());
font.setColor(color);
if(fontSize>0&&(null!=fontFamily||!"".equals(fontFamily))){
font.setSize(fontSize);
font.setFamily(fontFamily);
}
return font;
}
/**
* @Description 指定颜色字体 默认处理中文显示
* @Date 2019/7/12 14:05
* @param color 字体颜色
* @return com.itextpdf.text.Font
**/
public static Font getColorFont(BaseColor color) {
return getColorFont(color, 0, null);
}
/**
* @Description 默认处理中文显示
* @Date 2019/7/12 14:05
* @return com.itextpdf.text.Font
**/
public static Font getColorFont() {
Font font = new Font(getFont());
return font;
}
/**
* @Description 指定列宽度
* @Date 2019/7/12 11:59
* @param widths 一个或多个
* @return float[]
**/
public static float[] getColumnWiths(float...widths){
float[] columnWidths = new float[widths.length];
for (int i = 0; i < widths.length; i++) {
columnWidths[i]=widths[i];
}
return columnWidths;
}
/**
* @Description 添加表头cell
* @Date 2019/7/12 11:36
* @param titleCell 要操作的cell
* @param fixedHeight 行高度
* @param baseColor 背景色
* @param colspan 合并的列数
* @param isBottomBorder 是否有下边框 true 有 fasle 没有
* @return com.itextpdf.text.pdf.PdfPCell
**/
public static PdfPCell addTitleCell(PdfPCell titleCell,int fixedHeight,BaseColor baseColor,int colspan,boolean isBottomBorder){
titleCell.setColspan(colspan);
titleCell.setFixedHeight(fixedHeight);
titleCell.setUseVariableBorders(true);
titleCell.setUseAscender(true);
titleCell.setUseDescender(true);
titleCell.setBackgroundColor(baseColor);
if(isBottomBorder){
titleCell.setBorder(Rectangle.BOTTOM);
titleCell.setBorderColorBottom(BaseColor.LIGHT_GRAY);
}else{
titleCell.setBorder(Rectangle.NO_BORDER);
}
titleCell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
return titleCell;
}
/**
* @Description 添加空行
* @Date 2019/7/12 11:36
* @param fixedHeight 空行高度
* @param colspan 合并的列数
* @return com.itextpdf.text.pdf.PdfPCell
**/
public static PdfPCell addBlankLine(int fixedHeight,int colspan){
PdfPCell blankLine = new PdfPCell();
blankLine.setFixedHeight(fixedHeight);
blankLine.setBorder(Rectangle.NO_BORDER);
blankLine.setColspan(colspan);
return blankLine;
}
/**
* @Description 添加默认cell
* @param baseCell 要操作的cell
* @Date 2019/7/12 11:36
* @return com.itextpdf.text.pdf.PdfPCell
**/
public static PdfPCell addBaseCell(PdfPCell baseCell){
baseCell.setFixedHeight(23);
baseCell.setUseVariableBorders(true);
baseCell.setUseAscender(true);
baseCell.setUseDescender(true);
baseCell.setBorder(Rectangle.BOTTOM);
baseCell.setBorderColorBottom(BaseColor.LIGHT_GRAY);
baseCell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
return baseCell;
}
/**
* @Description 添加cell
* @param baseCell 要操作的cell
* @param isBottomBorder 是否有下边框 true 有 fasle 没有
* @Date 2019/7/12 11:36
* @return com.itextpdf.text.pdf.PdfPCell
**/
public static PdfPCell addBaseCell(PdfPCell baseCell,boolean isBottomBorder){
baseCell.setFixedHeight(23);
baseCell.setUseVariableBorders(true);
baseCell.setUseAscender(true);
baseCell.setUseDescender(true);
if(isBottomBorder){
baseCell.setBorder(Rectangle.BOTTOM);
baseCell.setBorderColorBottom(BaseColor.LIGHT_GRAY);
}else{
baseCell.setBorder(Rectangle.NO_BORDER);
}
baseCell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
return baseCell;
}
/**
* @Description 添加cell
* @param baseCell 要操作的cell
* @param fixedHeight 行高
* @param color 背景色
* @param isBottomBorder 是否有下边框 true 有 fasle 没有
* @Date 2019/7/12 11:36
* @return com.itextpdf.text.pdf.PdfPCell
**/
public static PdfPCell addBaseCell(PdfPCell baseCell,int fixedHeight,BaseColor color,boolean isBottomBorder){
baseCell.setFixedHeight(fixedHeight);
baseCell.setUseVariableBorders(true);
baseCell.setUseAscender(true);
baseCell.setUseDescender(true);
if(null!=color){
baseCell.setBackgroundColor(color);
}
if(isBottomBorder){
baseCell.setBorder(Rectangle.BOTTOM);
baseCell.setBorderColorBottom(BaseColor.LIGHT_GRAY);
}else{
baseCell.setBorder(Rectangle.NO_BORDER);
}
baseCell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
return baseCell;
}
/**
* @Description 设置中文支持
* @Date 2019/7/11 10:33
* @Param []
* @return com.itextpdf.text.pdf.BaseFont
**/
public static BaseFont getFont() {
BaseFont bf = null;
try {
bf = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
} catch (Exception e) {
System.out.println("Exception = " + e.getMessage());
}
return bf;
}
/**
* 斜角排列、全屏多个重复的花式文字水印
*
* @param input 需要加水印的PDF读取输入流
* @param output 输出生成PDF的输出流
* @param waterMarkString 水印字符
* @param xAmout x轴重复数量
* @param yAmout y轴重复数量
* @param opacity 水印透明度
* @param rotation 水印文字旋转角度,一般为45度角
* @param waterMarkFontSize 水印字体大小
* @param color 水印字体颜色
*/
public static void stringWaterMark(InputStream input, OutputStream output, String waterMarkString, int xAmout, int yAmout, float opacity, float rotation, int waterMarkFontSize, BaseColor color) {
try {
PdfReader reader = new PdfReader(input);
PdfStamper stamper = new PdfStamper(reader, output);
// 添加中文字体
BaseFont baseFont = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
int total = reader.getNumberOfPages() + 1;
PdfContentByte over;
// 给每一页加水印
for (int i = 1; i < total; i++) {
Rectangle pageRect = stamper.getReader().getPageSizeWithRotation(i);
// 计算水印每个单位步长X,Y
float x = pageRect.getWidth() / xAmout;
float y = pageRect.getHeight() / yAmout;
over = stamper.getOverContent(i);
PdfGState gs = new PdfGState();
// 设置透明度为
gs.setFillOpacity(opacity);
over.setGState(gs);
over.saveState();
over.beginText();
over.setColorFill(color);
over.setFontAndSize(baseFont, waterMarkFontSize);
for (int n = 0; n < xAmout + 1; n++) {
for (int m = 0; m < yAmout + 1; m++) {
over.showTextAligned(Element.ALIGN_CENTER, waterMarkString, x * n, y * m, rotation);
}
}
over.endText();
}
stamper.close();
} catch (Exception e) {
new Exception("NetAnd PDF add Text Watermark error"+e.getMessage());
}
}
/**
* 图片水印,整张页面平铺
* @param input 需要加水印的PDF读取输入流
* @param output 输出生成PDF的输出流
* @param imageFile 水印图片路径
*/
public static void imageWaterMark(InputStream input, OutputStream output, String imageFile, float opacity) {
try {
PdfReader reader = new PdfReader(input);
PdfStamper stamper = new PdfStamper(reader, output);
Rectangle pageRect = stamper.getReader().getPageSize(1);
float w = pageRect.getWidth();
float h = pageRect.getHeight();
int total = reader.getNumberOfPages() + 1;
Image image = Image.getInstance(imageFile);
image.setAbsolutePosition(0, 0);// 坐标
image.scaleAbsolute(w, h);
PdfGState gs = new PdfGState();
gs.setFillOpacity(opacity);// 设置透明度
PdfContentByte over;
// 给每一页加水印
float x, y;
Rectangle pagesize;
for (int i = 1; i < total; i++) {
pagesize = reader.getPageSizeWithRotation(i);
x = (pagesize.getLeft() + pagesize.getRight()) / 2;
y = (pagesize.getTop() + pagesize.getBottom()) / 2;
over = stamper.getOverContent(i);
over.setGState(gs);
over.saveState();//没这个的话,图片透明度不起作用,必须在beginText之前,否则透明度不起作用,会被图片覆盖了内容而看不到文字了。
over.beginText();
// 添加水印图片
over.addImage(image);
}
stamper.close();
} catch (Exception e) {
new Exception("NetAnd PDF add image Watermark error" + e.getMessage());
}
}
/**
* @description 顶部表格卡片形式显示格式数据组装
* @param tableMobileHeader 要操作的表格
* @param cellMobileHeader 要操作的单元格
* @param clospan 合并列 不需要合并填写0
* @param fixedHeight 行高
* @param padding 间距
* @param border 边框
* @param borderColor 边框颜色
* @param backgroundColor 背景色
* @param vertical 垂直对齐方式
* @param horizontal 水平对齐方式
* @return void
**/
public static void addTableHeaderData(PdfPTable tableMobileHeader, PdfPCell cellMobileHeader, int clospan, float fixedHeight, int padding, int border, BaseColor borderColor, BaseColor backgroundColor, int vertical, int horizontal) {
cellMobileHeader.setUseBorderPadding(true);
cellMobileHeader.setUseAscender(true);
if(clospan>0){
cellMobileHeader.setColspan(clospan);
}
cellMobileHeader.setUseDescender(true);
cellMobileHeader.setFixedHeight(fixedHeight);
cellMobileHeader.setPadding(padding);
cellMobileHeader.setVerticalAlignment(vertical);
cellMobileHeader.setHorizontalAlignment(horizontal);
if(null!=backgroundColor){
cellMobileHeader.setBackgroundColor(backgroundColor);
}
cellMobileHeader.setBorder(border);
cellMobileHeader.setBorderColor(borderColor);
tableMobileHeader.addCell(cellMobileHeader);
}
/**
* @description 顶部表格卡片形式显示格式数据组装
* @param tableMobileHeader 要操作的表格
* @param cellMobileHeader 要操作的单元格
* @param clospan 合并列 不需要合并填写0
* @param backgroundColor 背景色
* @return void
**/
public static void addTableHeaderData(PdfPTable tableMobileHeader, PdfPCell cellMobileHeader, int clospan, BaseColor backgroundColor) {
addTableHeaderData(tableMobileHeader, cellMobileHeader, clospan, 100, 10, 30, BaseColor.WHITE, backgroundColor, 0, 0);
}
}
生成代码:
@Async
public Future<Result> createPdf(List<Map<String, Object>> userMap,String relativePath) {
ByteArrayOutputStream out = null;
InputStream pdfStream = null;
try {
Document document = new Document(PageSize.A4, 50, 50, 30, 20);
out = new ByteArrayOutputStream();
PdfWriter writer = PdfWriter.getInstance(document,out);
document.open();
for (Map<String, Object> userInfo : userMap) {
CompanyStaffDto companyStaffDto = (CompanyStaffDto) userInfo.get("userInfo");
// 加入水印
PdfContentByte waterMar = writer.getDirectContentUnder();
// 开始设置水印
waterMar.beginText();
// 设置水印透明度
PdfGState gs = new PdfGState();
// 设置填充字体不透明度为0.4f
gs.setFillOpacity(0.2f);
try {
// 设置水印字体参数及大小 (字体参数,字体编码格式,是否将字体信息嵌入到pdf中(一般不需要嵌入),字体大小)
waterMar.setFontAndSize(PdfUtil.kaiTiFont,10);
// 设置透明度
waterMar.setGState(gs);
// 设置水印对齐方式 水印内容 X坐标 Y坐标 旋转角度
for(int i=0 ; i<3; i++) {
for (int j = 0; j < 6; j++) {
waterMar.showTextAligned(Element.ALIGN_CENTER,"水印内容", 50.5f + i * 280, 40.0f + j * 100, -15);
}
}
// 设置水印颜色
waterMar.setColorFill(BaseColor.GRAY);
//结束设置
waterMar.endText();
waterMar.stroke();
}catch (Exception e){
e.printStackTrace();
}finally {
waterMar = null;
gs = null;
}
//设置标题! 这个标题不是页面内,而是文件展示的时候左上角的
document.addTitle("标题");
//创建中文字体,否侧不能使用 参数1 内容 参数2 内容大小
Paragraph title = PdfUtil.createChineseFont("中文", 13);
//设置居中
title.setAlignment(Element.ALIGN_CENTER);
//将内容添加的节点中
document.add(title);
//添加QR图片
Image image = Image.getInstance((byte[]) userInfo.get("qrUrl"));
//设置图片所占大小
image.scaleAbsolute(60f, 60f);
image.setAlignment(Element.ALIGN_RIGHT);
image.setPaddingTop(-9f);
document.add(image);
//生成横线 linWidth 宽度
LineSeparator solidLine = new LineSeparator(2f, 100, BaseColor.BLACK, Element.ALIGN_CENTER, -5f);
document.add(solidLine);
LineSeparator dottedLine = new LineSeparator(1f, 100, BaseColor.BLACK, Element.ALIGN_CENTER, -8f);
document.add(dottedLine);
//打印时间设置
Paragraph printTime = PdfUtil.createChineseFont("打印时间" + DateUtil.format(new Date(), "yyyy年MM月dd日 HH:mm:ss"), 9);
//设置上间距
printTime.setSpacingBefore(3f);
//设置下间距
printTime.setSpacingAfter(3f);
//设置居中方式
printTime.setAlignment(Element.ALIGN_RIGHT);
document.add(printTime);
//设置基本情况页面
Paragraph basicSituation = PdfUtil.createChineseFont("☞ 基本情况", 10);
basicSituation.setAlignment(Element.ALIGN_LEFT);
document.add(basicSituation);
//创建基本情况表格
//创建表格对象
PdfPTable table = new PdfPTable(4);
table.setSpacingBefore(10);//前边距
table.setSpacingAfter(10);//后边距
table.setWidthPercentage(100);//表格宽占比
table.setHorizontalAlignment(Element.ALIGN_CENTER);
table.setHeaderRows(2);
table.getDefaultCell().setVerticalAlignment(Element.ALIGN_TOP);//单元格中文字垂直对齐方式
table.getDefaultCell().setBorderColor(BaseColor.BLACK);//单元格线条颜色
table.getDefaultCell().setMinimumHeight(30);//单元格最小高度
table.getDefaultCell().setExtraParagraphSpace(5);//段落文字与表格之间的距离,底部距离
table.getDefaultCell().setLeading(15, 0);//设置行间距
//这里使用的是自定义的Util工具类
PdfUtil.createCell(table, "姓名");
PdfUtil.createCell(table,"姓名数据");
PdfUtil.createCell(table, "报告月份");
PdfUtil.createCell(table, nowDate);
PdfUtil.createCell(table, "性别");
PdfUtil.createCell(table, "性别数据");
PdfUtil.createCell(table, "身份证号");
PdfUtil.createCell(table, "身份证号数据");
PdfUtil.createCell(table, "岗位");
PdfUtil.createCell(table, "岗位数据");
PdfUtil.createCell(table, "手机号");
PdfUtil.createCell(table, "手机号数据");
PdfUtil.createCell(table, "实际时长/计划时长");
PdfUtil.createCell(table, "实际时长/计划时长数据");
PdfUtil.createCell(table, "是否完成");
PdfUtil.createCell(table,"已完成" );
PdfUtil.createCell(table, "所属企业");
PdfUtil.createCell(table, Rectangle.BOX, 1, "企业名称数据", 8, 1, 3);
document.add(table);
/* 这里是尾部签名及日期的展示,就不加入了
//签名
Paragraph sign = PdfUtil.createChineseFont("签字:" + companyStaffDto.getName(), 8);
sign.setAlignment(Element.ALIGN_RIGHT);
document.add(sign);
//日期
Paragraph date = PdfUtil.createChineseFont(DateUtil.format(new Date(), "yyyy年MM月dd日"), 8);
date.setAlignment(Element.ALIGN_RIGHT);
document.add(date);
*/
try{
document.close();
}catch (Exception e){
log.debug("打印pdf无内容");
}
Result resultUrl =new Result();
pdfStream = new ByteArrayInputStream(out.toByteArray());
Result result = ossApiService.upload(pdfStream, relativePath);
if(result.success){
resultUrl = result;
}
return AsyncResult.forValue(resultUrl);
}catch (Exception e){
e.printStackTrace();
}finally {
try {
if(pdfStream!=null){
pdfStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if(out!=null){
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
最终的一个基本样式就是这个样子
问题汇总
1.1 为什么前端不能使用多图打印及数据量的情况
答: 1. 因为前端如果数据量大的话
会给浏览器造成很大的压力,会产生卡死的情况,这种情况当然是不愿意去看到的
2. 图片渲染量过多,会出现图片渲染丢失的情况,读取不到url而导致控制台页面报错,
越往后的图片越有几率会变成一个 X
这种情况下其实也有解决办法: 将我们的图片提前在本地以base64的方式去存储 ,
但是又回到第一个回答上,所以酌情使用.
1.2 什么时候适合使用前端打印,什么时候适合后端模板呢
答: 1. 什么时候可以使用前端打印功能:
1.1 像打印一般段落文字,承诺书这种,前端简单粗暴,也好布局
1.2 打印表格一类的文档,因为表格后端绘制比较麻烦,所以推荐交给前端使用<table/>去布局
1.3 轻量打印,数据量不大的前两种
1.4 前端样式设置的很精美,后端做到同等底部太难
2. 前端打印和后端打印其实各自能做到的,各自都能做到,而且效率和布局上前端更胜一筹,但是像一些样式复杂的,随着数据量增大,
前端导入的效率就无法和后端去相对应了,尤其是带有图片的情况下.而且不方便提供各种文件格式的
数据,打印页面情况下就交给后端去生成固定死的页面,还能更好的处理性能上问题
3.1 PDF文件中中文无法正常显示
答: 因为字体的原因,所以如果想使用中文字体的话,需要指定对应中文的编码 这里定义了一个工具类用于返回中文字体
public static BaseFont bfChinese;
private static final int defaultSize =12;
public static BaseFont kaiTiFont;
static {
try {
//指定字体 没有这个则 createChineseFont 方法无用
bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H",
BaseFont.NOT_EMBEDDED);
} catch (DocumentException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 返回中文Paragraph 默认12
* @param text
* @return
*/
public static Paragraph createChineseFont(String text){
Font c = new Font(bfChinese, defaultSize, Font.NORMAL);
return new Paragraph(text, c);
}
/**
* 返回中文Paragraph
* @param text
* @param size
* @return
*/
public static Paragraph createChineseFont(String text,int size){
if (size==0){
size=defaultSize;
}
Font c = new Font(bfChinese, size, Font.NORMAL);
return new Paragraph(text, c);
}
3.2 想使用自定义字体怎么办?
答: 自定义字体,不能通过简单的使用编码去调用,这点可以去看官方的Asian编码jar包,里面对中文支持少的可怜,更别说是字体,所以想使用,如楷体这类的需要自己去引用,这里提供一个楷体ttf文件,其他类型需要自行下载
代码里的文件我放在了resources文件下直接就可以通过相对路径去访问,但是excel读取字体的方式不能通过这样.excel Font用的是java的,pdf Font类是自己封装的 后面再说
public static BaseFont kaiTiFont;
static {
try {
//指定字体 没有这个则 createChineseFont 方法无用
kaiTiFont = BaseFont.createFont( "/ttf/KAITIGB2312.ttf",BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
} catch (DocumentException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 返回中文楷体Paragraph 默认12
* @param text
* @return
*/
public static Paragraph createChineseFont(String text){
Font c = new Font(kaiTiFont, defaultSize, Font.NORMAL);
return new Paragraph(text, c);
}
3.3 怎样给PDF中添加水印功能
具体PDF中操作水印,还是比较复杂,这个需要自己琢磨,这里给个简单的具体的可以看前面工具类中提供的水印方式
// 加入水印
PdfContentByte waterMar = writer.getDirectContentUnder();
// 开始设置水印
waterMar.beginText();
// 设置水印透明度
PdfGState gs = new PdfGState();
// 设置填充字体不透明度为0.4f
gs.setFillOpacity(0.2f);
try {
// 设置水印字体参数及大小 (字体参数,字体编码格式,是否将字体信息嵌入到pdf中(一般不需要嵌入),字体大小)
waterMar.setFontAndSize(PdfUtil.kaiTiFont,10);
// 设置透明度
waterMar.setGState(gs);
// 设置水印对齐方式 水印内容 X坐标 Y坐标 旋转角度
for(int i=0 ; i<3; i++) {
for (int j = 0; j < 6; j++) {
waterMar.showTextAligned(Element.ALIGN_CENTER,"水印内容", 50.5f + i * 280, 40.0f + j * 100, -15);
}
}
// 设置水印颜色
waterMar.setColorFill(BaseColor.GRAY);
//结束设置
waterMar.endText();
waterMar.stroke();
}catch (Exception e){
e.printStackTrace();
}finally {
waterMar = null;
gs = null;
}
3.4 如何给PDF表格中批量添加图片?
答: 这里给出一个思路,把表格分为n列, 每列放置一个图片 就不用考虑 格式的问题了
for (int i=0;i< size;i++) {
Image img = Image.getInstance(r.faceRecordDtoList.get(i).getFaceUrl() + "?x-oss-process=image/resize,m_fill,h_80,w_70/quality,q_70");
img.scaleAbsolute(50f, 60f);
img.setAlignment(Rectangle.LEFT);
img.setScaleToFitHeight(false);
PdfPCell imgCell = new PdfPCell(img);
imgCell.setColspan(1);
imgCell.setBorder(Rectangle.BOTTOM);
//行对象
safe.addCell(imgCell);
}
3.5 动态生成的PDF如何实现分页?
两种方案
1. 使用 document.newPage(); 就能从内容结束后进行分页
2. 另外一种,换个思路生成单个的pdf文件,然后使用工具把pdf拼接成一个pdf文件
3.6 评论的多图片处理思路,及缩小处理时间
1. 处理方式
当成单元格就行,循环N,注意排列方式 方向,换行这些就行.
比如 16个图片, 一行放5个
就 从左到右排列, 每隔5个换行操作, 排列到第4行的时候,剩余1个图片,
如果按照从左到右排列,那就是最左边
这个样子:
11111
11111
1
如果想居中可以采用,前面放2个空单元
2. 缩小处理时间
说实话, 图片是处理pdf最耗时的地方,如果没有图片,速度会非常快
(要读取图片),而图片对象的生成是必不可缺少的IO操作,尽量避免
多IO. 而IText处理过程中,我们是没办法介入的, 只能从图片下手,
缩小图片的大小,像素来达到目的, 举例阿里oss 就可以给地址链
接后缀参数来处理图片,这是个思路,如果有其他办法欢迎沟通