Java生成pdf文件或jpg圖片的案例講解

在一些業務場景中,需要生成pdf文件或者jpg圖片,有時候還需要帶上水印。我們可以事先用freemarker定義好html模板,然後把模板轉換成pdf或jpg文件。

同時freemarker模板還支持變量的定義,在使用時可以填充具體的業務數據。

1、Maven導包

<parent>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-parent</artifactId>
 <version>2.1.4.RELEASE</version>
</parent>
<dependencies>
 <!-- freemarker -->
 <dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring-context-support</artifactId>
 </dependency>
 <dependency>
 <groupId>org.freemarker</groupId>
 <artifactId>freemarker</artifactId>
 </dependency>
 <!-- pdf核心包 -->
 <dependency>
 <groupId>com.itextpdf</groupId>
 <artifactId>itextpdf</artifactId>
 <version>5.5.12</version>
 </dependency>
 <!-- 適配中文字體 -->
 <dependency>
 <groupId>com.itextpdf</groupId>
 <artifactId>itext-asian</artifactId>
 <version>5.2.0</version>
 </dependency>
 <!-- html轉pdf -->
 <dependency>
 <groupId>com.itextpdf.tool</groupId>
 <artifactId>xmlworker</artifactId>
 <version>5.5.12</version>
 </dependency>
 <!-- pdf轉圖片 -->
 <dependency>
 <groupId>org.apache.pdfbox</groupId>
 <artifactId>pdfbox</artifactId>
 <version>2.0.5</version>
 </dependency>
</dependencies>

2、接口定義

2.1、請求

@Data
public class GeneratePdfReq {
 /**
 * 生成pdf文件的絕對路徑
 */
 @NotBlank(message = "生成pdf文件的絕對路徑不能為空")
 @Pattern(regexp = "^.*(\\.pdf|\\.jpg)$", message = "生成的文件必須以.pdf或.jpg結尾")
 private String absolutePath;
 /**
 * 使用html模板的絕對路徑
 */
 @NotBlank(message = "使用的模板路徑不能為空")
 private String templateName;
 /**
 * 渲染模板的業務數據
 */
 private Object dataModel;
 /**
 * 水印信息
 */
 private WaterMarkInfo waterMarkInfo;
 /**
 * pdf文件的寬,默認A4
 */
 private float width = 595;
 /**
 * pdf文件的高,默認A4
 */
 private float height = 842;
}

2.2、水印

@Data
public class WaterMarkInfo {
 /**
 * 如果為null設置水印時會報錯
 */
 private String waterMark = "";
 /**
 * 水印透明度,值越小透明度越高
 */
 private float opacity = 0.2F;
 /**
 * 水印字體,如果亂碼設置為本地宋體字體:fonts/simsun.ttc,1
 */
 private String fontName = "STSong-Light";
 /**
 * 水印編碼格式,如果亂碼設置為:BaseFont.IDENTITY_H
 */
 private String encoding = "UniGB-UCS2-H";
 /**
 * 字體大小
 */
 private float fontSize = 24;
 /**
 * 橫坐標在頁面寬度的百分比,左下角為原點
 */
 private float x = 50;
 /**
 * 縱坐標在頁面高度的百分比,左下角為原點
 */
 private float y = 40;
 /**
 * 水印旋轉角度
 */
 private float rotation = 45;
}

2.3、響應

@Data
public class GeneratePdfResp {
 /**
 * 生成pdf的絕對路徑
 */
 private String absolutePath;
}

3、應用代碼

3.1、渲染freemarker模板獲取html網頁

@Service("freeMarkerService")
@Slf4j
public class FreeMarkerServiceImpl implements FreeMarkerService {
 @Autowired
 private FreeMarkerConfigurer freeMarkerConfigurer;
 /**
 * 渲染html後獲取整個頁面內容
 *
 * @param templatePath 模板路徑
 * @param dataModel 業務數據,一般以map形式傳入
 * @return
 */
 @Override
 public String getHtml(String templatePath, Object dataModel) {
 log.info("開始將模板{}渲染為html,業務數據{}", templatePath, JSONUtil.toJsonPrettyStr(dataModel));
 Configuration cfg = freeMarkerConfigurer.getConfiguration();
 cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); // freemaker異常時仍舊拋出,統一異常處理
 cfg.setClassicCompatible(true);// 不需要對null值預處理,否則需要在模板取值時判斷是否存在,不然報錯
 StringWriter stringWriter = new StringWriter();
 try {
  // 設置模板所在目錄,絕對路徑方式,不打進jar包
//  cfg.setDirectoryForTemplateLoading(new File(templatePath).getParentFile());
//  Template temp = cfg.getTemplate(new File(templatePath).getName());
  // 相對路徑設置模板所在目錄,模板打進jar包,默認就是resources目錄下的/templates目錄。
  cfg.setClassForTemplateLoading(this.getClass(), "/templates");
  Template temp = cfg.getTemplate(templatePath);
  temp.process(dataModel, stringWriter);
 } catch (Exception e) {
  log.error(PdfErrorCode.PDF_TEMPLATE_RENDER_FAIL.getDesc(), e);
  throw new PdfBizException(PdfErrorCode.PDF_TEMPLATE_RENDER_FAIL);
 }
 return stringWriter.toString();
 }
}

3.2、將html網頁轉pdf,並添加水印

@Service("pdfService")
@Slf4j
public class PdfServiceImpl implements PdfService {
 public static final String FONT_PATH = "fonts/simsun.ttc,1";
 @Autowired
 private WaterMarkerService waterMarkerService;
 /**
 * html頁面內容轉pdf,並給每頁附上水印
 *
 * @param html  html頁面內容
 * @param width  pdf的寬
 * @param height pdf的高
 * @param waterMarkInfo 水印信息
 * @return
 */
 @Override
 public byte[] html2Pdf(String html, float width, float height, WaterMarkInfo waterMarkInfo) {
 log.info("=================開始將html轉換為pdf=================");
 ByteArrayOutputStream out = new ByteArrayOutputStream();
 this.html2Pdf(html, width, height, out);
 byte[] bytes = out.toByteArray();
 // 設置水印
 if (waterMarkInfo != null) {
  bytes = waterMarkerService.addWaterMarker(bytes, waterMarkInfo);
 }
 return bytes;
 }
 /**
 * html轉pdf
 *
 * @param html html頁面內容
 * @param width pdf的寬
 * @param height pdf的高
 * @param out 輸出流,pdf文件用此流輸出,需要pdf文檔關閉後流中才會有數據
 */
 @Override
 @SneakyThrows
 public void html2Pdf(String html, float width, float height, OutputStream out) {
 @Cleanup Document document = new Document(new RectangleReadOnly(width, height)); // 默認A4縱向
 // 這裡需要關閉document才能讓生成的pdf字節數據刷到輸出流中
 PdfWriter writer = PdfWriter.getInstance(document, out); // 關閉可能導致生成的pdf顯示異常(Chrome)
 document.open();
 // 設置字體,這裡統一用simsun.ttc即宋體
 XMLWorkerFontProvider asianFontProvider = new XMLWorkerFontProvider() {
  @Override
  public Font getFont(String fontname, String encoding, boolean embedded, float size, int style, BaseColor color, boolean cached) {
  Font font;
  try {
   font = new Font(BaseFont.createFont(FONT_PATH, BaseFont.IDENTITY_H, BaseFont.EMBEDDED));
  } catch (Exception e) {
   log.error(PdfErrorCode.SET_PDF_FONT_FAIL.getDesc(), e);
   throw new PdfBizException(PdfErrorCode.SET_PDF_FONT_FAIL);
  }
  font.setStyle(style);
  font.setColor(color);
  if (size > 0) {
   font.setSize(size);
  }
  return font;
  }
 };
 // 生成pdf
 try {
  XMLWorkerHelper.getInstance().parseXHtml(writer, document, new ByteArrayInputStream(html.getBytes("UTF-8")), null, Charset.forName("UTF-8"), asianFontProvider);
  // 如果系統已經裝有simsun.ttc字體,則不需要單獨設置字體也不需要itext-asian jar包
//  XMLWorkerHelper.getInstance().parseXHtml(writer, document, new ByteArrayInputStream(html.getBytes("UTF-8")), null, Charset.forName("UTF-8"));
 } catch (RuntimeWorkerException e) {
  log.error(PdfErrorCode.HTML_CONVERT2PDF_FAIL.getDesc(), e);
  throw new PdfBizException(PdfErrorCode.HTML_CONVERT2PDF_FAIL);
 }
 }
}

添加水印實現類

@Service("waterMarkerService")
@Slf4j
public class WaterMarkerServiceImpl implements WaterMarkerService {
 /**
 * 給pdf文件每頁添加水印
 *
 * @param source pdf文件的字節數組形式
 * @param waterMarkInfo 水印信息
 * @return
 */
 @Override
 public byte[] addWaterMarker(byte[] source, WaterMarkInfo waterMarkInfo) {
 log.info("開始設置水印數據{}", JSONUtil.toJsonPrettyStr(waterMarkInfo));
 ByteArrayOutputStream out = new ByteArrayOutputStream();
 this.addWaterMarker(source, waterMarkInfo, out);
 return out.toByteArray();
 }
 /**
 * 給pdf文件每頁添加水印
 *
 * @param source pdf文件的字節數組形式
 * @param waterMarkInfo 水印信息
 * @param out  輸出流,pdf文件用此流輸出,需要pdf文檔關閉後流中才會有數據
 */
 @Override
 @SneakyThrows
 public void addWaterMarker(byte[] source, WaterMarkInfo waterMarkInfo, OutputStream out) {
 @Cleanup PdfReader reader = new PdfReader(source);
 // 這裡需要關閉PdfStamper才能讓生成的pdf字節數據刷到輸出流中
 @Cleanup PdfStamper pdfStamper = new PdfStamper(reader, out);
 BaseFont font = BaseFont.createFont(waterMarkInfo.getFontName(), waterMarkInfo.getEncoding(), BaseFont.EMBEDDED);
 PdfGState gs = new PdfGState();
 gs.setFillOpacity(waterMarkInfo.getOpacity());
 // 給每頁pdf生成水印
 for (int i = 1; i <= reader.getNumberOfPages(); i++) {
  PdfContentByte waterMarker = pdfStamper.getUnderContent(i);
  waterMarker.beginText();
  // 設置水印透明度
  waterMarker.setGState(gs);
  // 設置水印字體和大小
  waterMarker.setFontAndSize(font, waterMarkInfo.getFontSize());
  // 設置水印位置、內容、旋轉角度
  float X = reader.getPageSize(i).getWidth() * waterMarkInfo.getX() / 100;
  float Y = reader.getPageSize(i).getHeight() * waterMarkInfo.getY() / 100;
  waterMarker.showTextAligned(Element.ALIGN_CENTER, waterMarkInfo.getWaterMark(), X, Y, waterMarkInfo.getRotation());
  // 設置水印顏色
  waterMarker.setColorFill(BaseColor.GRAY);
  waterMarker.endText();
 }
 }
}

3.3、整合實現

@Slf4j
@Service("generatePdfService")
public class GeneratePdfServiceImpl implements RestService {
 @Autowired
 private FreeMarkerService freeMarkerService;
 @Autowired
 private PdfService pdfService;
 @Override
 @SneakyThrows
 public GeneratePdfResp service(GeneratePdfReq generatePdfReq) {
 log.info("開始生成pdf文件,請求報文:{}", JSONUtil.toJsonPrettyStr(generatePdfReq));
 /*
 1.根據freemarker模板填充業務數據獲取完整的html字符串
  */
 String html = freeMarkerService.getHtml(generatePdfReq.getTemplateName(), generatePdfReq.getDataModel());
 /*
 2.生成pdf文件(內存)
  */
 byte[] bytes = pdfService.html2Pdf(html, generatePdfReq.getWidth(), generatePdfReq.getHeight(), generatePdfReq.getWaterMarkInfo());
 /*
 3.本地保存pdf文件
  */
 File targetFile = new File(generatePdfReq.getAbsolutePath());
 // 上級目錄不存在則創建
 if (!targetFile.getParentFile().exists()) {
  targetFile.getParentFile().mkdirs();
 }
 // 根據不同文件名後綴生成對應文件
 if (generatePdfReq.getAbsolutePath().endsWith("pdf")) {
  FileUtils.writeByteArrayToFile(targetFile, bytes);
 } else {
  @Cleanup PDDocument document = PDDocument.load(bytes);
  PDFRenderer renderer = new PDFRenderer(document);
  BufferedImage bufferedImage = renderer.renderImageWithDPI(0, 150);// 隻打第一頁,dpi越大圖片越高清也越耗時
  ByteArrayOutputStream baos = new ByteArrayOutputStream();
  ImageIO.write(bufferedImage, "jpg", baos);
  FileUtils.writeByteArrayToFile(targetFile, baos.toByteArray());
 }
 log.info("文件本地保存完成,文件路徑:[{}]", targetFile.getAbsolutePath());
 /*
 4.組織返回
  */
 GeneratePdfResp generatePdfResp = new GeneratePdfResp();
 generatePdfResp.setAbsolutePath(targetFile.getAbsolutePath());
 return generatePdfResp;
 }
}

3.4、controller

@Slf4j
@RestController
public class PdfController {
 @Autowired
 private RestService generatePdfService;
 @PostMapping(value = "/html2Pdf")
 public GeneratePdfResp html2Pdf(@RequestBody @Validated GeneratePdfReq req) {
 GeneratePdfResp resp = generatePdfService.service(req);
 return resp;
 }
}

4、應用

4.1、freemarker模板(html模板)

<html>
<head>
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
 <meta http-equiv="Content-Style-Type" content="text/css"/>
 <style>
 body {
  font-family: SimSun
 }
 </style>
 <title>html模板</title>
</head>
<body>
<div>
 <p style="margin:0pt; orphans:0; text-align:center; widows:0">
 <span style="font-family:SimSun; font-size:16pt">html模板</span><br/>
 </p>
 <p>姓名:${name}</p>
 <p>證件號碼:${cardNo}</p>
 <p>日期:${date}</p>
</div>
</body>
</html>

4.2、接口調用生成pdf

5、說明

1、根據參數後綴名可以生成pdf或jpg文件,生成的pdf文件默認為A4大小,也可以通過請求參數設置大小。

2、pdf文件會根據html模板內容大小自動分頁。

3、如果生成圖片,多頁不會生成多張圖片,可以把高度設置大一些,最後會生成長圖。

4、水印每頁都會自動添加。

5、為瞭提高代碼的復用性和可維護性,工程內渲染html模板、生成pdf文件、添加水印都有單獨的接口實現。

代碼地址

github:https://github.com/senlinmu1008/spring-boot/tree/master/html2pdf

gitee:https://gitee.com/ppbin/spring-boot/tree/master/html2pdf

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。如有錯誤或未考慮完全的地方,望不吝賜教。