1. 程式人生 > >freemarker + ItextRender 根據模板生成PDF檔案

freemarker + ItextRender 根據模板生成PDF檔案

1. 製作模板

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">  
<html xmlns="http://www.w3.org/1999/xhtml">
<head lang="en">
  <meta http-equiv="content-type" content="text/html;charset=utf-8"></meta>
  <title>軟體包隱患掃描報告</title>
  <style>
    .template_ftl {
      width: 100%;
      height: 100%;
      padding: 30px 25px;
    }

    .template_box {
      width: 600px;
      height: 842px;
      box-sizing: border-box;
      margin: 0 auto;
    }

    .template_ftl .template_header {

      width: 100%;
      height: 50px;
      box-sizing: border-box;
      color: #0D122B;
    }

    .template_ftl .template_header > i {
      display: inline-block;
      width: 24px;
      height: 28px;
      background: url("stack.png") no-repeat center center;
    }

    .template_ftl .template_header > h2 {
      display: inline-block;
      font-size: 17px;
    }

    .template_ftl .template_header > div {
      float: right;
      color: #B7C1CF;
      font-size: 9px;
      line-height: 50px;
    }

    .template_ftl .template_header > div > i {
      display: inline-block;
      width: 9px;
      height: 9px;
      background: url("Calendar.png") no-repeat center center;
    }

    .template_ftl .template_description {
      width: 100%;
      margin-top: 10px;
    }

    .template_ftl .template_title {
      width: 100%;
      height: 18px;
      line-height: 18px;
      color: #00AEA1;
      font-size: 12px;
    }

    .template_ftl .template_title > i {
      display: inline-block;
      width: 14px;
      height: 14px;
      margin-right: 10px;
      background: url("Layers.png") no-repeat center center;
    }

    .template_ftl .template_info {
      list-style: none;
      padding-left: 30px;
    }

    .template_ftl .template_info label {
      display: inline-block;
      width: 80px;
      margin-right: 30px;
      color: #646464;
      font-size: 10px;
      vertical-align: middle;
    }

    .template_ftl .template_info span {
      color: #0D122B;
      font-size: 10px;
      vertical-align: middle;
    }

    .template_ftl .template_info li {
      margin-bottom: 5px;
    }

    .template_ftl .template_title .shield {
      width: 14px;
      height: 14px;
      background: url("Shield.png") no-repeat center center;
    }

    .template_ftl .template_title .screen {
      width: 14px;
      height: 14px;
      background: url("Screen.png") no-repeat center center;
    }

    .template_ftl .template_network {
      list-style: none;
      padding-left: 30px;
      margin-top: 10px;
    }

    .template_ftl .template_network .network_level b {
      color: #0D122B;
      font-size: 12px;
    }

    .template_ftl .template_network .network_level span {
      color: #E74C3C;
      font-size: 12px;
    }

    .template_ftl .template_network .network_level p {
      margin: 0;
      color: #646464;
      font-size: 10px;
    }

    .template_ftl .template_network .network_content:before {
      content: '● ';
      color: #00AEA1;
    }

    .template_ftl .template_network .network_content {
      color: #3E4550;
      font-size: 10px;
      margin-top: 10px;
    }

    .online_info {
      width: 100%;
    }

    .online_info .online_list {
      width: 100%;
     
      box-sizing: border-box;
      list-style: none;
      padding-left: 30px;
      display: flex;
      flex-flow: row nowrap;
      justify-content: space-between;
      align-items: center;
      margin: 0;
      color:#3E4550;
      font-size: 10px;
    }
    .online_info .online_list:nth-child(2n){
      background-color: #EFEFEF;
    }
    .online_info .online_list > td {
      width: 90px;
      line-height: 25px;
      text-align: center;
     
    }
    .online_info .online_list > td:first-child {
      text-align: center;
    }
     
  </style>
</head>
<body>
<div class="template_ftl" style="font-family:SimSun;">

  <div class="template_box">
    <div class="template_header">
      <i></i>

      <span style="font-size:24px;font-weight:1px bold;">軟體包掃描報告</span>

      <div>
        <i></i>
        <span>
        	<#if (currentDate)??>
        		${currentDate}
        	</#if>
        </span>
      </div>
    </div>
    <div class="template_description">
      <div class="template_title">
        <i></i>
        <span>掃描概要資訊</span>
      </div>
      <ul class="template_info">
        <li>
          <label>掃描產品名稱</label>
          <span>
          	<#if (softScanInfo.productName)??>
				${softScanInfo.productName}
			</#if>
		 </span>
        </li>
        <li>
          <label>檔案路徑</label>
          <span>
		  		<#if (softScanInfo.scanFilePath)??>
					${softScanInfo.scanFilePath}
				</#if>
		  </span>
        </li>
        <li>
          <label>掃描開始時間</label>
          <span>
          	<#if (softScanInfo.scanBegintime)??>
				${(softScanInfo.scanBegintime)?string("yyyy年MM月dd日 HH:mm:ss")}
			</#if>
          </span>
        </li>
        <li>
          <label>掃描結束時間</label>
          <span>
          	<#if (softScanInfo.scanEndtime)??>
				${(softScanInfo.scanEndtime)?string("yyyy年MM月dd日 HH:mm:ss")}
			</#if>
          </span>
        </li>
      </ul>
    </div>
    <div class="template_description">
      <div class="template_title">
        <i class="shield"></i>
        <span>軟體包安全資訊</span>
      </div>
      <ul class="template_network">
        <li class="network_level">
          <span style="font-size:13px;color:black;">軟體包風險等級為 </span>
		  
		  	<#if (softScanInfo.scanResult)??>
				<#if (softScanInfo.scanResult == '高')>
					<span style="color:red;">${softScanInfo.scanResult}</span>
					<#elseif (softScanInfo.scanResult == '中')>
						<span style="color:#FF9900;">${softScanInfo.scanResult}</span>
					<#elseif (softScanInfo.scanResult == '低')>
						<span style="color:#0000FF;">${softScanInfo.scanResult}</span>
					<#else> 
						<span style="color:#00cc00;">安全</span>
				</#if>
			<#else>
				<span style="color:#00cc00;">安全</span>
		    </#if>
          <p style="margin-top:8px;">本次共掃描1個軟體包,其中包含<#if recordList?exists>${recordList?size} <#else>0</#if>款軟體</p>
        </li>
		<li class="network_content">
          <span>
          		風險等級為<span style="color:red;">高</span>的軟體數為
				<span style="color:red;"><#if (highRiskRecord.appScanResult)??>${highRiskRecord.appScanResult}</span></#if>
		  </span>
        </li>
		<li class="network_content">
          <span>
          		風險等級為<span style="color:#FF9900;">中</span>的軟體數為
				<span style="color:#FF9900;"><#if (middleRiskRecord.appScanResult)??>${middleRiskRecord.appScanResult}</span></#if>
		  </span>
        </li>
		<li class="network_content">
          <span>
          		風險等級為<span style="color:#0000FF;">低</span>的軟體數為
				<span style="color:#0000FF;"><#if (lowRiskRecord.appScanResult)??>${lowRiskRecord.appScanResult}</span></#if>
		  </span>
        </li>
		<li class="network_content">
          <span>
          		風險等級為<span style="color:#00cc00;">安全</span>的軟體數為
				 <span style="color:#00cc00;"><#if (noneRiskRecord.appScanResult)??>${noneRiskRecord.appScanResult}</span></#if>
		  </span>
        </li>
      </ul>
    </div>
    <div class="template_description">
      <div class="template_title">
        <i class="screen"></i>
        <span>掃描軟體包概要資訊</span>
      </div>
      <div class="online_info" >
	     <table class="online_list" style="word-break: break-all; word-wrap: break-word;">
	        <tr class="online_list" style="border-bottom: 1px solid #B7C1CF;color: #94949;">
	          <td >軟體名稱</td>
	          <td style="width:120px;">版本號</td>
	          <td style="width:150px;">軟體包名稱</td>
	          <td style="width:40px;">安全風險</td>
	          <td style="width:150px;">安全描述事件</td>
	          <td style="width:30px;">通過</td>
	        </tr>
	        <#if recordList?exists>
				<#list recordList as record>
					<tr class="online_list" >
						<td>
							<#if (record.appName)??>
								${record.appName}
							</#if>
						</td>
				        <td style="width:120px;">
				        	<#if (record.appVersion)??>
								${record.appVersion}
							</#if>
				        </td>
				        <td style="width:150px;">
				        	<#if (record.appFileName)??>
								${record.appFileName}
							</#if>
				        </td>
				        <td style="width:30px;">
				        	<#if (record.riskRating)??>
				        		<#if (record.riskRating == '無')>
				        			安全
				        		<#else>
									${record.riskRating}
								</#if>
							<#else>
								安全
							</#if>
				        </td>
				        <td style="width:150px;">
							<#if (record.riskDescription)??>
								${record.riskDescription}
							<#else>
								無
							</#if>
						</td>
				        <td style="width:20px;">
				        	<#if (record.appScanResult)??>
				        		<#if (record.appScanResult = 1)>
						           <i style="color: #00AEA1">√</i>
				        		<#else>
				        			<i style="color: #E74C3C">×</i>
				        		</#if>
							<#else>
								<i style="color: #00AEA1">√</i>
							</#if>
				        </td>
					</tr>
				</#list>
			</#if>
		</table>
      </div>
    </div>
  </div>
</div>
</body>
</html>

2. 獲取模板,並將所獲取的資料載入生成html檔案

public String getHtmlString (final Object data, final String templateDirectory, final String templateName, final String encoding) {
	log.info("FreeMarkerUtil!");
	String html = null;
	StringWriter stringWriter = null;
	BufferedWriter writer = null;
	try {
		//建立一個負責管理FreeMarker模板的Configuration例項
		final Configuration configuration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
		//指定FreeMarker模板檔案位置
//		configuration.setDirectoryForTemplateLoading(new File(templateDirectory));
			
		//設定模板的編碼格式
		configuration.setEncoding(Locale.CHINA, encoding);
		//獲取模板
		configuration.setClassForTemplateLoading(this.getClass(), "/templates");
		final Template template = configuration.getTemplate(templateName, encoding);
		stringWriter = new StringWriter();
		writer = new BufferedWriter(stringWriter);
		//將資料輸出到html中
		template.process(data, writer);
		writer.flush();
		html = stringWriter.toString();
	} catch (Exception e) {
		log.error("failed to get html String!", e);
	} finally {
		if (stringWriter !=null) {
			try {
				stringWriter.close();
			} catch (IOException e) {
				log.error("failed to close writer!", e);
			}
		}
		if (writer !=null) {
			try {
				writer.close();
			} catch (IOException e) {
				log.error("failed to close writer!", e);
			}
		}
	}
	log.info("getHtmlString() exit!");
	return html;
}

2. 生成PDF檔案

final File file = new File(reportPath);
try {
     if (!file.exists()) {
          file.createNewFile();
     }
     final String chineseFont = getChineseFont();//獲取字型檔案路徑
     log.info("chineseFont=" + chineseFont);
     createPDF(new FileOutputStream(file), chineseFont, html);
     
} 
public void createPDF(final OutputStream outputStream, final String fontPath, final String html) {
        final ITextRenderer renderer = new ITextRenderer();
        // 設定字型樣式
        try {
            renderer.getFontResolver().addFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
            //這裡的字型必須和ftl模板中的字型一致,否則中文會丟失
            // 把html程式碼傳入渲染器中
            renderer.setDocumentFromString(html);
            /*
             * 解決圖片的相對路徑問題,必須在設定document後再設定圖片路徑,不然不起作用,
             * 如果使用絕對路徑依然有問題,可以在路徑前面加"file:/"
             */
            // renderer.getSharedContext().setBaseURL("file:/C:/Users/HYH/Desktop/templateftp/");
            renderer.getSharedContext().setBaseURL("file:" + getPictureDirctory());
            log.info("images directory path: file:" + getPictureDirctory());

            renderer.layout();
            renderer.createPDF(outputStream, false);
            renderer.finishPDF();
            outputStream.flush();
        } catch (final Exception e) {
            log.error("failed to create pdf!", e);
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (final IOException e) {
                    log.error("failed to close outputStream!", e);
                }
            }
        }
        log.info("create pdf completely!");
    }

其中由兩個地方需要注意,都是關於獲取檔案路徑的問題,由於專案部署的時候是打包成jar包形式,所以在開發過程中時直接安照傳統的獲取方法沒有一點檔案,但是當打包後部署,總是出錯。於是參考網上文章,先將檔案讀出來到專案的臨時目錄下,然後再按正常方式載入該臨時檔案;

//獲取圖片目錄位置
    private String getPictureDirctory () {
    	final File fileDir = new File("System.getProperty("user.dir") + "/images/"");
    	if (!fileDir.exists()) {
    		fileDir.mkdirs();
    		final InputStream calendarStream = getClass().getClassLoader().getResourceAsStream("images/Calendar.png");
    		final InputStream deleteStream = getClass().getClassLoader().getResourceAsStream("images/delete.png");
    		final InputStream layersStream = getClass().getClassLoader().getResourceAsStream("images/Layers.png");
    		final InputStream saveStream = getClass().getClassLoader().getResourceAsStream("images/save.png");
    		final InputStream screenStream = getClass().getClassLoader().getResourceAsStream("images/Screen.png");
    		final InputStream shieldStream = getClass().getClassLoader().getResourceAsStream("images/Shield.png");
    		final InputStream stackStream = getClass().getClassLoader().getResourceAsStream("images/stack.png");
    		
    		final File calendarFile = new File(IMAGES_TEMP_PATH + "Calendar.png");
    		final File deleteFile = new File(IMAGES_TEMP_PATH + "delete.png");
    		final File layersFile = new File(IMAGES_TEMP_PATH + "Layers.png");
    		final File saveFile = new File(IMAGES_TEMP_PATH + "save.png");
    		final File screenFile = new File(IMAGES_TEMP_PATH + "Screen.png");
    		final File shieldFile = new File(IMAGES_TEMP_PATH + "Shield.png");
    		final File stackFile = new File(IMAGES_TEMP_PATH + "stack.png");
    		
    		try {
				FileUtils.copyInputStreamToFile(calendarStream, calendarFile);
				FileUtils.copyInputStreamToFile(deleteStream, deleteFile);
				FileUtils.copyInputStreamToFile(layersStream, layersFile);
				FileUtils.copyInputStreamToFile(saveStream, saveFile);
				FileUtils.copyInputStreamToFile(screenStream, screenFile);
				FileUtils.copyInputStreamToFile(shieldStream, shieldFile);
				FileUtils.copyInputStreamToFile(stackStream, stackFile);
			} catch (IOException e) {
				log.error("圖片檔案複製失敗!", e);
			}
    	}
    	return IMAGES_TEMP_PATH;
    }
/**
     * 獲取中文字型位置
     * @return
     */
    private String getChineseFont() {

        final InputStream stream = getClass().getClassLoader().getResourceAsStream("fonts/simsun.ttf");
        final File fileDir = new File("System.getProperty("user.dir") + "/fonts/"");
        if (!fileDir.exists()) {
            fileDir.mkdirs();
        }
        final String fontsPath = FONTS_TEMP_PATH + "simsun.ttf";
        final File targetFile = new File(fontsPath);
        try {
            FileUtils.copyInputStreamToFile(stream, targetFile);
        } catch (final IOException e) {
            log.error("字型檔案複製失敗!", e);
        }
        return fontsPath;
    }

還有一個問題至今沒有解決,就是關於生成PDF檔案後自動換行的問題,參考了網上大多數的作法,都是修改模板樣式,生成的html確實是可以換行,但是對PDF無效。最後想出了一個比較笨也不是很合適的方法,在後臺先給它在某處先換好行,然後再生成PDF,雖然比較笨,也存在問題,但是確實解了燃眉之急。

/**
     * 向字串source中每隔sep個插入一個replace
     * @param source
     * @param sep
     * @param replace
     * @return
     */
    public String changeLine(final String source, final int sep, final String replace) {
    	if (source == null) {
			return null;
		}
    	final StringBuffer buffer = new StringBuffer(source);
		for (int i = sep; i < source.length(); i+=(sep + 1)) {
			buffer.insert(i, replace);
		}
		return buffer.toString();
	}

最終結果


如果有大神知道如何正確實現自動換行,還請不吝賜教。