HTML 轉 PDF

幾種HTML轉PDF工具的對比

工具 特點
html2image 簡單html轉化,對CSS的支持不好
itextpdf 需要自己寫模板,可以動態填充
wkhtmltopdf 轉化速度快,效果好

所以此處我們重點將wkhtmltopdf的使用做一個示例,完整的項目地址在末尾的鏈接處

使用

springboot是現在開發的主流框架,所以此處主要是示例在springboot項目中如何集成,其他項目請自行參考使用

準備

需要準備三個基礎的文件,分別如下:

  • simsun.ttc:字體文件
  • wkhtmltopdf.exe:轉換工具,window系統下使用,適用於64爲系統,32位系統自行去官網下載對應版本
  • wkhtmltox:轉換工具,Linux系統下使用,同樣適用於64位系統

將以上三個文件拷貝到springboot的resources根目錄,具體的文件可到文章末尾的項目地址鏈接中獲取,如下圖:

pom依賴

<!-- lombok -->
<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<version>1.18.12</version>
	<scope>provided</scope>
</dependency>
<!-- junit -->
<dependency>
	<groupId>junit</groupId>
	<artifactId>junit</artifactId>
	<version>4.12</version>
	<scope>test</scope>
</dependency>
<!-- commons-fileupload -->
<dependency>
	<groupId>commons-fileupload</groupId>
	<artifactId>commons-fileupload</artifactId>
	<version>1.3.1</version>
</dependency>
<!-- 獲取系統信息 -->
<dependency>
	<groupId>net.java.dev.jna</groupId>
	<artifactId>jna</artifactId>
	<version>5.4.0</version>
</dependency>

<!-- 好用的工具類 -->
<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.4.0</version>
</dependency>

新建工具類

import com.sun.jna.Platform;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemHeaders;
import org.apache.commons.fileupload.util.FileItemHeadersImpl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;

import cn.hutool.core.io.FileUtil;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;

/**
  * Html轉PDF的工具類
  * @author zhongyj <[email protected]><br/>
  * @date 2020/8/29
  */
@Slf4j
public class Html2PdfUtils {

    private static final File WK_HOME_DIR = FileUtil.file(FileUtil.getUserHomePath()+"/wkHome");

    private static final File WK_TMP_DIR = FileUtil.file(FileUtil.getTmpDirPath()+"/wkTemp");

    private static final File SIM_SUN_FONT_DIR = Platform.isLinux() ? FileUtil.file("/usr/share/fonts/chinese/TrueType")
            : FileUtil.file("C:\\Windows\\Fonts");

    private static File wkTool;
    private static File simSunFont;

    private static boolean canUse = true;
    private static boolean able = true;


    static {
        log.info("Tools are only  available for Windows 64 and Linux 64 platforms !!!");
        boolean init = forceInit();
        log.info("Tools init result: {}", init);
    }

    /**
     * 初始化
     * @return  是否初始化成功
     */
    public static boolean forceInit() {
        long initc = 0L;
        if (!WK_HOME_DIR.exists()) {
            able = WK_HOME_DIR.mkdirs();
        }
        log.info("{},check wkHomeDir ,result:{}", ++initc, able);
        if (!WK_TMP_DIR.exists()) {
            able = WK_TMP_DIR.mkdirs();
        }
        log.info("{},check wkTmpDir ,result:{}", ++initc, able);

        if (!SIM_SUN_FONT_DIR.exists()) {
            able = SIM_SUN_FONT_DIR.mkdirs();
        }
        log.info("{},check simsunFontDir ,result:{}", ++initc, able);

        InputStream wkHtmlToxAsStream = null;
        InputStream simSunAsStream = null;
        if (able) {
            wkHtmlToxAsStream = Platform.isLinux() ? Html2PdfUtils.class.getResourceAsStream("/wkhtmltox") : Html2PdfUtils.class.getResourceAsStream("/wkhtmltopdf.exe");
            simSunAsStream = Html2PdfUtils.class.getResourceAsStream("/simsun.ttc");
        }
        if (null == wkHtmlToxAsStream || simSunAsStream == null) {
            log.error("{},load wkHtmlToxAsStream :{},load simSunAsStream:{}", ++initc, null == wkHtmlToxAsStream, simSunAsStream == null);
            able = false;
        }
        log.info("{},load wktool and font source ,result:{}", ++initc, able);

        if (able) {
            File font = new File(SIM_SUN_FONT_DIR, "simsun.ttc");
            File wk = new File(WK_HOME_DIR, Platform.isLinux() ? "wkhtmltox" : "wkhtmltopdf.exe");

            try {
                if (!font.exists()) {
                    assert simSunAsStream != null;
                    able = 1 < Files.copy(simSunAsStream, font.toPath(), StandardCopyOption.REPLACE_EXISTING);
                }
                log.info("{},copy font source to {},result:{}", ++initc, font.toPath(), able);
                if (!wk.exists()) {
                    assert wkHtmlToxAsStream != null;
                    able = 1 < Files.copy(wkHtmlToxAsStream, wk.toPath(), StandardCopyOption.REPLACE_EXISTING);
                }
                log.info("{},copy wktools source to {},result:{}", ++initc, font.toPath(), able);

                if (able) {
                    wkTool = wk;
                    simSunFont = font;
                }
            } catch (IOException e) {
                e.printStackTrace();
                able = false;
                log.error("{}, error when copy source : {} ", ++initc, e.getMessage());
            }
        }

        if (able) {
            if (Platform.isLinux()) {
                boolean canExe = exePermissionCheck();
                log.info("{},check run permission,result: {}  ", ++initc, canExe ? "has permission" : "no permission");
                if (!canExe) {
                    simpleExecCommand("chmod +x " + wkTool.getPath());
                    if (!exePermissionCheck()) {
                        log.error("{},add permission failed", ++initc);
                        able = false;
                    }
                }
            }
        }
        if (able) {
            able = cleanTempDir();
        }
        if (able) {
            log.info("{},init success!", ++initc);
        } else {
            log.info("{},init failed!", ++initc);
            canUse = false;
        }
        return able;
    }

    public String getSimsunPath() {
        log.info("world path:" + simSunFont.getPath());
        return simSunFont.getPath();
    }

    public static Html2PdfUtils build() {
        return new Html2PdfUtils();
    }

    private static boolean exePermissionCheck() {
        String permissionLog = simpleExecCommand("ls -l " + wkTool.getPath());
        return null != permissionLog && permissionLog.length() >= 10 && 120 == permissionLog.charAt(9);
    }

    private static boolean cleanTempDir() {
        if (WK_TMP_DIR.exists()) {
            canUse = deleteFiles(WK_TMP_DIR) ? WK_TMP_DIR.mkdirs() : canUse;
            log.info("cleanTempDir,result:{} ", canUse);
        } else {
            canUse = WK_TMP_DIR.mkdirs();
        }
        return canUse;
    }

    public synchronized FileItem convertPdfFromText(String text, String fileName) {
        cleanTempDir();
        if (!canUse) {
            log.info("tools crash,can invoke forceInit() method see reason !!!");
            return null;
        }
        File html = new File(WK_TMP_DIR, fileName + ".html");
        File pdf = new File(WK_TMP_DIR, fileName + ".pdf");

        // 將html字符串寫入到臨時的html文件
        FileUtil.writeUtf8String(text, html);

        if (html.exists() && html.isFile()) {
            log.info("exec html to  pdf ,wktoolPath=>{}", wkTool.getPath());
            simpleExecCommand(wkTool.getPath() + " " + html.getPath() + " " + pdf.getPath());
        }
        if (pdf.exists() && pdf.isFile()) {
            try (FileInputStream fileInputStream = new FileInputStream(pdf); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
                byte[] buffer = new byte[2014];
                while (fileInputStream.read(buffer) != -1) {
                    byteArrayOutputStream.write(buffer);
                }
                byteArrayOutputStream.flush();
                byte[] data = byteArrayOutputStream.toByteArray();
                if (data.length > 0) {
                    log.info("html to  pdf  success ");
                    SimplePdfFileItem file = new SimplePdfFileItem(pdf, data, Files.probeContentType(pdf.toPath()), "file");
                    log.info("pdf size :{}", file.getSize());
                    return file;
                }
            } catch (IOException e) {
                log.error("html to  pdf  failed");
                e.printStackTrace();
                return null;
            }
        }
        log.info("html to  pdf  failed,no data");
        return null;
    }

    private static boolean deleteFiles(File file) {
        if (!file.exists()) {
            log.info("del the file:{},is not exists", file.getPath());
            return false;
        }
        if (file.isFile()) {
            return file.delete();
        }
        File[] subFiles = file.listFiles();
        if (null != subFiles && subFiles.length > 0) {
            Arrays.asList(subFiles).forEach(Html2PdfUtils::deleteFiles);
        }

        return file.delete();
    }

    private static String simpleExecCommand(String cmd) {
        try {
            String[] linux = {"/bin/sh", "-c", cmd};
            String[] windows = {"cmd", "/c", cmd};
            String[] cmdA = Platform.isLinux() ? linux : windows;
            Process process = Runtime.getRuntime().exec(cmdA);
            LineNumberReader br = new LineNumberReader(new InputStreamReader(process.getInputStream()));
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = br.readLine()) != null) {
                log.info(line);
                sb.append(line).append("\n");
            }
            return sb.toString();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    @ToString
    static class SimplePdfFileItem implements FileItem {
        private static final long serialVersionUID = 2237570099615271025L;
        public static final String DEFAULT_CHARSET = "ISO-8859-1";
        private String fieldName;
        private final String fileName;
        private boolean isFormField;
        private final byte[] cachedContent;
        private final String contentType;
        private final File dFosFile;
        private FileItemHeaders headers;

        public SimplePdfFileItem(File dFosFile, byte[] cachedContent, String contentType, String fieldName) {
            this.fieldName = fieldName;
            this.fileName = dFosFile.getName();
            this.isFormField = false;
            this.cachedContent = null == cachedContent || cachedContent.length < 1 ? new byte[0] : cachedContent;
            this.contentType = contentType;
            this.dFosFile = dFosFile;
            this.headers = new FileItemHeadersImpl();
        }

        public SimplePdfFileItem(String fieldName, String fileName, boolean isFormField, byte[] cachedContent
                , String contentType, File dFosFile, FileItemHeaders headers) {
            this.fieldName = fieldName;
            this.fileName = fileName;
            this.isFormField = isFormField;
            this.cachedContent = cachedContent;
            this.contentType = contentType;
            this.dFosFile = dFosFile;
            this.headers = headers;
        }

        @Override
        public InputStream getInputStream() throws IOException {
            if (null == this.dFosFile) {
                return new ByteArrayInputStream(this.cachedContent);
            }
            return new FileInputStream(dFosFile);
        }

        @Override
        public String getContentType() {
            return this.contentType;
        }

        @Override
        public String getName() {
            return this.fileName;
        }

        @Override
        public boolean isInMemory() {
            return this.cachedContent.length > 0;
        }

        @Override
        public long getSize() {
            return this.cachedContent.length;
        }

        @Override
        public byte[] get() {
            return this.cachedContent;
        }

        @Override
        public String getString(String s) throws UnsupportedEncodingException {
            return getString();
        }

        @Override
        public String getString() {
            return new String(cachedContent, StandardCharsets.UTF_8);
        }

        @Override
        public void write(File file) throws Exception {
            Files.write(file.toPath(), cachedContent, StandardOpenOption.CREATE);
        }

        @Override
        public void delete() {
            boolean delete = dFosFile.delete();
        }

        @Override
        public String getFieldName() {
            return this.fieldName;
        }

        @Override
        public void setFieldName(String s) {
            this.fieldName = s;
        }

        @Override
        public boolean isFormField() {
            return this.isFormField;
        }

        @Override
        public void setFormField(boolean b) {
            this.isFormField = b;
        }

        @Override
        public OutputStream getOutputStream() throws IOException {
            if (null == this.dFosFile) {
                return new ByteArrayOutputStream(1024);
            }
            return new FileOutputStream(this.dFosFile);
        }

        @Override
        public FileItemHeaders getHeaders() {
            return this.headers;
        }

        @Override
        public void setHeaders(FileItemHeaders fileItemHeaders) {
            this.headers = fileItemHeaders;
        }

    }

}

轉換

@Test
public void down() throws UnsupportedEncodingException {
    String html = FileUtil.readUtf8String("E:\\入院記錄.html");
    FileItem sx = Html2PdfUtils.build().convertPdfFromText(html, "sx");
    log.info(sx.toString());
    byte[] bytes = sx.get();
    FileUtil.writeBytes(bytes,new File("E:\\入院記錄-1.pdf"));
}

項目示例地址:https://gitee.com/dimples9527/html2pdf

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章