项目背景
近期在做一个web项目的打印功能,本来都已经做好了,但是客户的使用场景是钉钉工作台内嵌的浏览器,由于钉钉目前不支持标准浏览器的打印功能,所以打印在钉钉内无法使用,故而寻找其他的解决办法:通过将页面导出为pdf,然后再打印。网上也已经有itext转pdf的例子,但是大多都是copy & paste,有些不能直接使用,并且转换效果不好,更直接的就是中文不能直接换行,导致表格直接超出一个A4纸的大小。经过大量的百度,我整理出了一个可行的方法,话不多说直接上代码,代码可直接copy使用。第一次写博客,不喜勿喷!!
效果图
开发环境
后端基于jdk 1.8 ,springboot2.0 前端基于ng-alain
资源
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.4.3</version>
</dependency>
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf-itext5</artifactId>
<version>9.1.18</version>
</dependency>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>fonts/*</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<includes>
<include>fonts/*</include>
</includes>
</resource>
</resources>
大段代码
前端代码
print.component.html
<div #content id="content">
<div class="page">
<h3 style="text-align: center">采购申请单</h3>
<div style="height:32px">
<div style="float: left;">打印时间:{{ dateTime | date:'yyyy-MM-dd HH:mm:ss'}}</div><div style="float: right;">打印人:{{ settings.user.realname }}</div>
</div>
<table class="purchaseTable">
<tr>
<td class="header">申请单号</td>
<td>{{purchase.code}}</td>
<td class="header">需求日期</td>
<td colspan="2">{{purchase.reqDate}}</td>
<td class="header">采购类别</td>
<td>{{purchase.category}}</td>
</tr>
<tr>
<td class="header">IT相关采购</td>
<td>{{purchase.itPurchase == '1'?'是':'否'}}</td>
<td class="header" style="white-space:nowrap;word-break: keep-all;">是否定向采购</td>
<td colspan="2">{{purchase.directPurchase == '1'?'是':'否'}}</td>
<td class="header" style="white-space:nowrap;word-break: keep-all;">是否自行采购</td>
<td>{{purchase.selfPurchase == '1'?'是':'否'}}</td>
</tr>
<tr>
<td class="header">需求部门</td>
<td colspan="2">{{purchase.pkDepartName}}</td>
<td class="header" colspan="2">申请人</td>
<td colspan="2">{{purchase.applicant}}</td>
</tr>
<tr>
<td class="header">项目采购/非项目采购</td>
<td>{{purchase.projectPurchase == '1'?'项目采购':'非项目采购'}}</td>
<td class="header">项目名称/预算名称</td>
<td colspan="2">
<ng-container *ngIf="purchase.projectPurchase == '1'; else elseName">
{{purchase.pkProjectName}}
</ng-container>
<ng-template #elseName>
{{purchase.budgetName}}
</ng-template>
</td>
<td class="header">项目余额/预算余额</td>
<td>
<ng-container *ngIf="purchase.projectPurchase == '1'; else elseAmount">
{{purchase.predictAmount}}
</ng-container>
<ng-template #elseAmount>
{{purchase.budget}}
</ng-template>
</td>
</tr>
<tr *ngIf="purchase.directPurchase == '1'">
<td class="header">定向原因</td>
<td colspan="6">{{purchase.directReson}}</td>
</tr>
<tr>
<td class="header">采购事由</td>
<td colspan="6">{{purchase.purchaseReason}}</td>
</tr>
<tr>
<td class="header">特殊需求说明</td>
<td colspan="6">{{purchase.specialExplain}}</td>
</tr>
<tr>
<td class="header">联系人</td>
<td>{{purchase.contactPsn}}</td>
<td class="header">联系电话</td>
<td>{{purchase.contactTel}}</td>
<td class="header">地址</td>
<td colspan="2">{{purchase.address}}</td>
</tr>
<tr>
<td class="header">备注</td>
<td colspan="6">{{purchase.memo}}</td>
</tr>
<tr>
<td class="header">附件</td>
<td colspan="6">
<ng-container *ngIf="purchase.fileList && purchase.fileList.length>0; else elseTemplate">
<div *ngFor="let item of purchase.fileList">
<i nz-icon nzType="file" nzTheme="outline"></i>{{item.name}}
</div>
</ng-container>
<ng-template #elseTemplate>
无
</ng-template>
</td>
</tr>
<tr>
<td class="header">产品编码</td>
<td class="header">产品名称</td>
<td class="header">单位</td>
<td class="header">技术参数</td>
<td class="header">数量</td>
<td class="header">预估含税单价</td>
<td class="header">预估总价</td>
</tr>
<tr *ngFor="let item of materialList">
<td class="text-center">{{item.materialCode}}</td>
<td class="text-center">{{item.materialName}}</td>
<td class="text-center">{{item.materialUnit}}</td>
<td class="text-center">{{item.materialTechParam}}</td>
<td class="text-center">{{item.num}}</td>
<td class="text-center">{{item.predictPrice}}</td>
<td class="text-center">{{item.totalPrice}}</td>
</tr>
<tr>
<td colspan="2" class="header">推荐供应商名称</td>
<td class="header">联系人</td>
<td colspan="2" class="header">联系电话</td>
<td colspan="2" class="header">邮箱</td>
</tr>
<tr *ngFor="let item of supplierList">
<td colspan="2" class="text-center" style="height: 39px;">{{item.supplierName}}</td>
<td class="text-center">{{item.supplierContact}}</td>
<td colspan="2" class="text-center">{{item.supplierContactTel}}</td>
<td colspan="2" class="text-center">{{item.supplierContactEmail}}</td>
</tr>
<tr>
<td class="header" rowSpan="opinions.length">审批信息</td>
<td colspan="6">
<ng-container *ngFor="let ta of opinions;let i = index">
<div *ngIf="i!=opinions.length-1" style="border-bottom:1px gray solid">
{{ta.personnelName}}:{{ta.opinion}}<span style="float: right;">{{ta.dealTime}}</span>
</div>
<div *ngIf="i==opinions.length-1">
{{ta.personnelName}}:{{ta.opinion}}<span style="float: right;">{{ta.dealTime}}</span>
</div>
</ng-container>
<div *ngIf="copyPerson!=null && copyPerson.length>0" style="border-top:1px gray solid">
抄送人:
<span *ngFor="let cp of copyPerson;">{{cp.personnelName}}</span>
</div>
</td>
</tr>
<tr>
<td class="header" rowSpan="data.length">评论信息</td>
<td colspan="6">
<ng-container *ngFor="let ts of data;let i = index">
<div *ngIf="i!=data.length-1" style="border-bottom:1px gray solid">
{{ts.personnelName}}:{{ts.text}}<div style="float: right;">{{ts.createTime}}</div>
</div>
<div *ngIf="i==data.length-1">
{{ts.personnelName}}:{{ts.text}}<div style="float: right;">{{ts.createTime}}</div>
</div>
</ng-container>
</td>
</tr>
</table>
</div>
</div>
<div class="drawer-footer">
<button type="button" (click)="exportPdf()" class="ant-btn" style="margin-right: 8px;">导出为PDF</button>
</div>
print.component.ts
import { Component, OnInit, ViewChild, Input, ElementRef } from '@angular/core';
import { _HttpClient, ModalHelper } from '@delon/theme';
import { SettingsService } from '@delon/theme';
import { environment } from '@env/environment';
import { DictService } from 'app/services/dict.service';
import { NzMessageService, NzNotificationService, NzDrawerRef } from 'ng-zorro-antd';
import { Purchase } from 'app/entity/basedata/purchase';
import { PDFViewerComponent } from '@shared/common/office/pdfviewer/pdfviewer.component';
import { PurchaseService } from 'app/services/basedata/purchase.service';
import { ProcessService } from 'app/services/process/process.service';
import { ProcessComment } from 'app/entity/process/processComment';
import { EssenceNg2PrintComponent } from 'essence-ng2-print';
import { DownloadService } from 'app/services/download.service';
@Component({
selector: 'app-purchase-req-printing',
templateUrl: './printing.component.html',
styles:[
`
.page {
width: 21cm;
min-height: 29.7cm;
padding: 2cm;
margin: 1cm auto;
border: 1px #D3D3D3 solid;
border-radius: 5px;
background: white;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
position: relative;
}
.header{
text-align:center;
}
.text-center{
text-align:center;
}
.purchaseTable{
width:100%;
word-wrap:break-word;
border: 1px gray solid;
border-spacing: 0px;
border-collapse: collapse;
line-height:36px;
}
.purchaseTable td{
padding:0 3px;
border: 1px gray solid;
}
`
]
})
export class PurchaseReqPrintingComponent implements OnInit {
purchaseCategorys: any;
appendixCategorys:any;
prefix = environment.SERVER_URL+"sys/common/downloadFile/";
constructor(
private dictService:DictService,
private modalHelper:ModalHelper,
private msg:NzMessageService,
private purchareService : PurchaseService,
private notification:NzNotificationService,
private drawerRef: NzDrawerRef,
private processService:ProcessService,
public settings: SettingsService,
private download:DownloadService
) {
this.printStyle = `
.purchaseTable{
width:100%;
word-wrap:break-word;
border: 1px gray solid;
border-spacing: 0px;
border-collapse: collapse;
line-height:36px;
}
.purchaseTable td{
border: 1px gray solid;
padding:0 3px;
}
.purchaseTable tr{
page-break-inside: avoid;
}
.header{
text-align:center;
}
.text-center{
text-align:center;
}
`
}
ngOnInit() {
//加载数据,代码略
}
@ViewChild('content', {read: false}) content: ElementRef;
exportPdf(){
const printHtml = document.getElementById('content').innerHTML;
// let preAddHtml = "<!DOCTYPE html[<!ENTITY nbsp ' '>]>";
let preAddHtml = "<html>";
preAddHtml += "<head>";
preAddHtml += "<meta http-equiv='Content-Type' content='text/html; charset=utf-8'></meta>";
preAddHtml += "<style type='text/css'>";
preAddHtml += ".page{size:a4}.purchaseTable{width:100%;word-wrap:break-word;border: 1px gray solid;border-spacing: 0px;border-collapse: collapse;line-height:36px;}";
preAddHtml += ".purchaseTable td{border: 1px gray solid;padding:0 3px;}";
preAddHtml += "@page{size:A4 }";
preAddHtml += "table{table-layout:fixed; word-break:break-strict;}";
preAddHtml += ".purchaseTable tr{page-break-inside: avoid;}";
preAddHtml += ".header{text-align:center;}";
preAddHtml += ".text-center{text-align:center;}";
preAddHtml += "</style>";
preAddHtml += "</head>";
preAddHtml += "<body style='font-family:SimSun;'>";
let suffixAddHtml = "</body>";
suffixAddHtml += "</html>";
const html = preAddHtml + printHtml + suffixAddHtml;
const body = {
htmlStr:html,
code:this.purchase.code
}
this.dictService.html2pdf(body).then((response)=>{
this.download.requestBlob(environment.SERVER_URL+"sys/common/downloadFile/"+response.result).subscribe(result => {
this.download.downFile(result,this.purchase.code+".pdf");
})
})
}
}
downloadService
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class DownloadService {
constructor(private http: HttpClient) { }
//Blob请求
requestBlob(url:any,data?:any):Observable<any>{
return this.http.request("get",url,{body: data, observe: 'response',responseType:'blob'});
}
//Blob文件转换下载
downFile(result,fileName,fileType?){
var data=result.body;
var blob = new Blob([data], {type: fileType||data.type});
var objectUrl = URL.createObjectURL(blob);
var a = document.createElement('a');
a.setAttribute('style', 'display:none');
a.setAttribute('href', objectUrl);
a.setAttribute('download', fileName);
a.click();
URL.revokeObjectURL(objectUrl);
}
}
pdf工具类
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.pdf.BaseFont;
import org.springframework.util.ResourceUtils;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
public class PDFUtil {
/**
* 通过html的字符串转pdf
* @param out
* @param html
* @throws IOException
* @throws DocumentException
*/
public static void createPdfByHtml(OutputStream out, String html) {
try {
ITextRenderer renderer = new ITextRenderer();
renderer.setDocumentFromString(html);
// 解决中文支持问题
ITextFontResolver fontResolver = renderer.getFontResolver();
fontResolver.addFont("/fonts/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
renderer.layout();
renderer.createPDF(out);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 通过html的文件路径转pdf
* @param out
* @param htmlFilePath
* @throws IOException
* @throws DocumentException
*/
public static void createPdfByUrl(OutputStream out,String htmlFilePath) {
try {
ITextRenderer renderer = new ITextRenderer();
String url = new File(htmlFilePath).toURI().toURL().toString();
renderer.setDocument(url);
// 解决中文支持问题
ITextFontResolver fontResolver = renderer.getFontResolver();
String fontPath = ResourceUtils.getURL("classpath:templates/font/simsun.ttc").getPath();
fontResolver.addFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
//解决图片的相对路径问题
//renderer.getSharedContext().setBaseURL("http://localhost:8080");//file:/e:/
renderer.layout();
renderer.createPDF(out);
} catch (Exception e) {
e.printStackTrace();
}
}
}
后端请求并把html字符串转换为pdf,存在服务器
/**
* html生成pdf
*/
@PostMapping(value = "/html2pdf")
public Result<String> html2pdf(@RequestBody Map<String,String> mp) throws Exception{
Result<String> result = new Result<>();
String htmlStr = mp.get("htmlStr");
String code = mp.get("code");
String ctxPath = uploadpath;
String bizPath = "html2pdf";
File file = new File(ctxPath + File.separator + bizPath);
if (!file.exists()) {
file.mkdirs();// 创建文件根目录
}
String pdfPath = ctxPath + File.separator + bizPath + File.separator + code + ".pdf";
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(new File(pdfPath)));
PDFUtil.createPdfByHtml(bos,htmlStr);
String filePath = bizPath + File.separator + code + ".pdf";
result.setResult(filePath);
result.setSuccess(true);
return result;
}
下载文件的接口
/**
* 获取文件
* 请求地址:http://localhost:8080/common/downloadFile/{file/20190119/e1fe9925bc315c60addea1b98eb1cb1349547719_1547866868179.jpg}
*
* @param request
* @param response
*/
@GetMapping(value = "/downloadFile/**")
public void downloadFile(HttpServletRequest request, HttpServletResponse response) {
// ISO-8859-1 ==> UTF-8 进行编码转换
String filePath = extractPathFromPattern(request);
// 其余处理略
InputStream inputStream = null;
OutputStream outputStream = null;
try {
filePath = filePath.replace("..", "");
if (filePath.endsWith(",")) {
filePath = filePath.substring(0, filePath.length() - 1);
}
String localPath = uploadpath;
String fileurl = localPath + File.separator + filePath;
String fileName = fileurl.substring(fileurl.lastIndexOf("/")+1);
//浏览器设置,处理下载文件时文件名乱码
String userAgent = request.getHeader("User-Agent");
if (userAgent.contains("MSIE") || userAgent.contains("Trident")) {
//IE浏览器处理
fileName = java.net.URLEncoder.encode(fileName, "UTF-8");
} else {
// 非IE浏览器的处理:
fileName = new String(fileName.getBytes("UTF-8"), "ISO-8859-1");
}
response.setContentType("application/octet-stream");
response.setHeader("content-type", "application/octet-stream");
response.addHeader("Content-Length", "" + new File(fileurl).length());
response.setHeader("Content-Disposition", "attachment;fileName=" + fileName );// 设置文件名
inputStream = new BufferedInputStream(new FileInputStream(fileurl));
outputStream = response.getOutputStream();
byte[] buf = new byte[1024];
int len;
while ((len = inputStream.read(buf)) > 0) {
outputStream.write(buf, 0, len);
}
response.flushBuffer();
} catch (IOException e) {
log.info("预览图片失败" + e.getMessage());
// e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
参考:https://www.jianshu.com/p/dd94d291ed57