项目背景

近期在做一个web项目的打印功能,本来都已经做好了,但是客户的使用场景是钉钉工作台内嵌的浏览器,由于钉钉目前不支持标准浏览器的打印功能,所以打印在钉钉内无法使用,故而寻找其他的解决办法:通过将页面导出为pdf,然后再打印。网上也已经有itext转pdf的例子,但是大多都是copy & paste,有些不能直接使用,并且转换效果不好,更直接的就是中文不能直接换行,导致表格直接超出一个A4纸的大小。经过大量的百度,我整理出了一个可行的方法,话不多说直接上代码,代码可直接copy使用。第一次写博客,不喜勿喷!!

效果图

钉钉 python上传文件可预览 钉钉文件怎么上传pdf_sed

开发环境

后端基于jdk 1.8 ,springboot2.0 前端基于ng-alain

资源

simsun.ttc

<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