HTML 转 PDF

几种HTML转PDF工具的对比

工具 特点
html2image 简单html转化,对CSS的支持不好
itextpdf 需要自己写模板,可以动态填充
wkhtmltopdf 转化速度快,效果好

所以此处我们重点将wkhtmltopdf的使用做一个示例,完整的项目地址在末尾的链接处

使用

springboot是现在开发的主流框架,所以此处主要是示例在springboot项目中如何集成,其他项目请自行参考使用

准备

需要准备三个基础的文件,分别如下:

  • simsun.ttc:字体文件
  • wkhtmltopdf.exe:转换工具,window系统下使用,适用于64为系统,32位系统自行去官网下载对应版本
  • wkhtmltox:转换工具,Linux系统下使用,同样适用于64位系统

将以上三个文件拷贝到springboot的resources根目录,具体的文件可到文章末尾的项目地址链接中获取,如下图:

image

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 <1126834403@qq.com><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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341