美文网首页
使用wkhtmltopdf和freemaker生成pdf

使用wkhtmltopdf和freemaker生成pdf

作者: Java分布式架构实战 | 来源:发表于2022-02-24 20:44 被阅读0次

    背景

    在CMS项目中,网页的正文是用富文本编辑器来维护的,经过调研发现,将文章生成word版本时,需要使用doc4j来生成,核心代码如下:

    ### 对html进行标准化处理并增加字符集设置
    Document document = org.jsoup.Jsoup.parse(htmlContent);
    document.head().prepend("<meta charset=\"utf-8\"/>");
    String normalizedHtmlContent = document.html();
    
    ### 将标准化后的html内容插入word文件
    WordprocessingMLPackage aPackage = WordprocessingMLPackage.load(outputFile);
    MainDocumentPart mainDocumentPart = aPackage.getMainDocumentPart();
    mainDocumentPart.addAltChunk(AltChunkType.Html, normalizedHtmlContent.getBytes(Charsets.UTF_8));
    aPackage.save(outputFile);
    

    本来想将word转换成pdf, 经过失败了。不知道其他朋友有没有遇到过类似的需求场景,如果有的话,希望你能给大家分享一下。

    最终转换思路,决定使用html来生成pdf, 经过一番调研,发现wkhtmltopdf还不错,能够较好地满足需求。

    尝试在命令行环境实现

    生成的pdf需要页眉页脚,其中页脚中的页码如果是奇数放在右边,如果是偶数放在左边,就像这样的效果:


    页码为奇数放在页脚右边
    页码为偶数放在页脚的左边

    通过阅读wkhtmltopdf文档发现,wkhtmltopdf在生成页脚时,使用GET请求获取页脚中页面的数据,同时会带上page等参数。考虑到要根据page来动态控制页脚中的页码,我决定使用freemarker来实现。因此需要在项目中引入freemaker并做好相关配置:

    # 添加freemarker依赖
    <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-freemarker</artifactId>
            </dependency>
    # 在application.properties中配置相关参数
    ############################## freemarker START ##############################
    spring.freemarker.enabled=true
    spring.freemarker.template-loader-path=classpath:/templates/
    spring.freemarker.suffix=.html
    spring.freemarker.cache=false
    spring.freemarker.charset=UTF-8
    spring.freemarker.content-type=text/html
    spring.freemarker.check-template-location=true
    spring.freemarker.settings.tag_syntax=square_bracket
    spring.freemarker.settings.template_update_delay=5
    spring.freemarker.settings.defaultEncoding=UTF-8
    spring.freemarker.settings.url_escaping_charset=UTF-8
    spring.freemarker.settings.locale=zh_CN
    spring.freemarker.settings.boolean_format=true,false
    spring.freemarker.settings.datetime_format=yyyy-MM-dd HH:mm:ss
    spring.freemarker.settings.date_format=yyyy-MM-dd
    spring.freemarker.settings.time_format=HH:mm:ss
    spring.freemarker.settings.number_format=0.######
    spring.freemarker.settings.whitespace_stripping=true
    spring.freemarker.settings.template_exception_handler=com.xxx.ow.staticpage.exception.FreemarkerExceptionHandler
    ############################## freemarker END ##############################
    
    • 添加Controller代码
    /**
     * 规章详情
     *
     * @author : jamesfu
     * @date : 21/2/2022 14:22
     */
    @Controller
    @RequestMapping("/api/page/ruleArticle")
    @Slf4j
    public class RuleArticleController {
        /**
         * pdf页眉
         *
         * @return 页面模板
         */
        @GetMapping("pdfHeader")
        public ModelAndView pdfHeader() {
            return new ModelAndView("custom/article/规章详情-PDF-Header");
        }
    
        /**
         * pdf页脚
         *
         * @return 页面模板
         */
        @GetMapping("pdfFooter")
        public ModelAndView pdfFooter(HttpServletRequest request, Model model) {
            log.info("request query string is {}", request.getQueryString());
            String strPage = request.getParameter("page");
            log.info("page is {}", strPage);
            if (strPage != null) {
                int intPage = Integer.parseInt(strPage);
                model.addAttribute("page", intPage);
            } else {
                model.addAttribute("page", 0);
            }
            return new ModelAndView("custom/article/规章详情-PDF-Footer");
        }
    
    /**
         * pdf页面
         *
         * @return 页面模板
         */
        @GetMapping("pdfBody")
        public ModelAndView pdfBody(Model model) {
            model.addAttribute("title","住房和城乡建设部关于修改《建设工程勘察质量管理办法》的决定");
            model.addAttribute("notes","(2021年4月1日中华人民共和国住房和城乡建设部令第53号公布 自公布之日起施行)");
            model.addAttribute("content","住房和城乡建设部决定对《建设工程勘察质量管理办法》(建设部令第115号,根据建设部令第163号修改)作如下修改:");
            return new ModelAndView("custom/article/规章详情-PDF-Body");
        }
    }
    
    • 添加freemarker pdfHeader模板
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8"/>
      <title>规章详情页眉</title>
      <style>
        html, body {
          margin: 0;
        }
    
        p {
          margin: 0;
          padding: 0;
        }
    
        .gz-detail {
          margin: auto 85px;
        }
    
        .gz-page-header {
          position: relative;
          padding-top: 69px;
          padding-bottom: 16px;
        }
    
        .gz-page-header .logo {
          width: 36px;
          display: inline-block;
          vertical-align: middle;
        }
    
        .gz-page-header .page-title {
          font-size: 24px;
          color: #005392;
          display: inline-block;
          vertical-align: middle;
          font-family: 'FangSong','仿宋', 'SimSun', '宋体';
          padding-left: 2px;
          font-weight: bold;
        }
    
        .gz-page-header .gz-detail-border-bottom {
          position: absolute;
          bottom: -5px;
          width: 100%;
          height: 3px;
          background: #015293;
          margin-bottom: 5px;
        }
      </style>
    </head>
    <body>
    <div class="gz-detail">
      <div class="gz-page-header">
        <img class="logo" src="/assets/image/logo/icon_logo.png">
        <span class="page-title">住房和城乡建设部规章</span>
        <div class="gz-detail-border-bottom"></div>
      </div>
    </div>
    </body>
    </html>
    
    • 添加freemarker pdfFooter模板
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
      <meta charset="utf-8"/>
      <title>规章详情页脚</title>
      <style>
        html, body {
          margin: 0;
        }
    
        p {
          margin: 0;
          padding: 0;
        }
        .gz-detail {
          margin: auto 85px;
        }
    
        .gz-page-footer {
          border-top: 3px solid #015293;
          padding-top: 22px;
          padding-bottom: 22px;
          font-size: 24px;
          text-align: right;
          color: #015293;
          /*padding-right: 63px;*/
          padding-right: 0;
          font-weight: bold;
          font-family: 'FangSong','仿宋', 'SimSun', '宋体';
        }
    
        .left-page {
          padding: 7px 0;
          text-align: left;
          font-size: 24px;
          font-family: 'FangSong','仿宋', 'SimSun', '宋体';
        }
    
        .right-page {
          padding: 7px 0;
          text-align: right;
          font-size: 24px;
          font-family: 'FangSong','仿宋', 'SimSun', '宋体';
        }
      </style>
    </head>
    <body>
    <div class="gz-detail">
      [#if page % 2 == 0]
      <div class="left-page">- ${page} -</div>
      [#else]
      <div class="right-page">- ${page} -</div>
      [/#if]
      <div class="gz-page-footer">住房和城乡建设部发布</div>
    </div>
    </body>
    </html>
    
    • 添加freemarker pdfBody模板
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
      <meta charset="utf-8"/>
      <title>规章详情正文</title>
      <style>
        html, body {
          margin: 0;
        }
    
        p {
          margin: 0;
          padding: 0;
        }
    
        .gz-detail {
          margin: auto 85px;
        }
    
        .gz-page-content {
          padding-top: 85px;
          position: relative;
        }
    
        .gz-page-content .content-title {
          text-align: center;
        }
    
        .gz-page-content .content-title .main-title,
        .gz-page-content .content-title .main-title * {
          font-size: 36px;
          font-family: 'SimSun', '宋体';
          color: #333333;
        }
    
        .gz-page-content .content-title .notes {
          font-size: 24px;
          padding: 14px 0 9px;
          margin-bottom: 30px;
          line-height: 150%;
          font-family: 'FangSong','仿宋', 'SimSun', '宋体';
          color: #333333;
        }
    
        .gz-page-content .editorContent-box {
          border: 0;
        }
    
        .gz-page-content .expire-box {
          position: absolute;
          right: 40px;
          top: 70px;
          text-align: center;
        }
    
        .gz-page-content .expire-box img {
          width: 189px;
          height: 76px;
        }
    
        .gz-page-content .expire-box .expire-text {
          padding-top: 5px;
          font-size: 14px;
          color: #757575;
        }
    
        .content-inner {
          width: 100%;
          height: auto;
          padding: 0 5px 40px;
          margin-top: 20px;
          overflow: hidden;
          font-size: 16px;
          line-height: 30px;
          box-sizing: border-box;
        }
    
        .content-inner div,
        .content-inner h1,
        .content-inner h2,
        .content-inner h3,
        .content-inner h4,
        .content-inner h5,
        .content-inner h6,
        .content-inner hr,
        .content-inner p,
        .content-inner blockquote,
        .content-inner dl,
        .content-inner dt,
        .content-inner dd,
        .content-inner ul,
        .content-inner ol,
        .content-inner pre,
        .content-inner span,
        .content-inner a,
        .content-inner i,
        .content-inner strong,
        .content-inner b,
        .content-inner form,
        .content-inner fieldset,
        .content-inner legend,
        .content-inner button,
        .content-inner input,
        .content-inner select,
        .content-inner textarea,
        .content-inner th,
        .content-inner td {
          font-size: inherit;
        }
    
        .content-inner a {
          color: #006AD2;
        }
    
        .content-inner img {
          max-width: 100%;
        }
    
        .content-inner p {
          line-height: 30px;
          margin-top: 15px;
        }
    
        .content-inner table,
        .content-inner img {
          margin: 0 auto;
        }
    
        .content-inner ol {
          list-style: decimal;
        }
    
        .content-inner ol li {
          list-style: inherit;
        }
    
        .content-inner ul {
          list-style: disc;
        }
    
        .content-inner ul li {
          list-style: inherit;
        }
      </style>
    </head>
    <body>
    <div class="gz-detail">
      <div class="gz-page-content">
        <div class="content-title">
          <p class="main-title">${title}</p>
          <p class="notes">${notes}</p>
        </div>
        <div class="content-inner">
          ${content}
        </div>
      </div>
    </div>
    </body>
    </html>
    
    • 使用wkhtmltopdf在命令行进行一次测试
    /usr/local/bin/wkhtmltopdf --page-size A4 --header-spacing 3 --footer-spacing 6  
    --header-html 'http://127.0.0.1:18080/api/page/ruleArticle/pdfHeader' 
    --footer-html 'http://127.0.0.1:18080/api/page/ruleArticle/pdfFooter'
    'http://127.0.0.1:18080/api/page/ruleArticle/pdfBody' 
    /Users/jamesfu/pdf/wengao022101.pdf
    

    实现效果还是不错的。


    wkhtmltopdf实现效果

    集成到后端服务中

    • 引用maven包java-wkhtmltopdf-wrapper
    <!--wkhtmltopdf包装器:https://github.com/jhonnymertz/java-wkhtmltopdf-wrapper-->
            <dependency>
                <groupId>com.github.jhonnymertz</groupId>
                <artifactId>java-wkhtmltopdf-wrapper</artifactId>
                <version>1.1.13-RELEASE</version>
            </dependency>
    
    • 使用java-wkhtmltopdf-wrapper生成文件,然后上传到文件服务器
     @Resource
        private IFileUploadService fileUploadService;
    
        @Resource
        private SiteConfig siteConfig;
    
        @Resource
        private BusinessConfig businessConfig;
    
        @Resource
        private Configuration configuration;
    
        /**
         * 生成规章pdf文件
         *
         * @return pdf文件上传结果
         */
        @Override
        public FileUploadResult generatePdf(Map<String, Object> data) throws TemplateException, IOException {
            //构造模板路径和输出文件路径
            String articleTitle = data.get("title").toString();
            String resultFileName = articleTitle + ".pdf";
    
            String outputDir = siteConfig.getStaticTempDir() + File.separator + "ruleArticle";
            String outputFilePath = outputDir + File.separator + resultFileName;
            FileUtil.mkdir(outputDir);
            File outputFile = FileUtil.file(outputFilePath);
    
            String headerHtml = String.format("http://127.0.0.1:%s/api/page/ruleArticle/pdfHeader", businessConfig.getServerPort());
            String footerHtml = String.format("http://127.0.0.1:%s/api/page/ruleArticle/pdfFooter", businessConfig.getServerPort());
            String bodyHtml = generateBodyHtml(data);
    
            try (FileOutputStream fos = new FileOutputStream(outputFile)) {
                String wkhtmltopdfCommand = businessConfig.getWkhtmltopdfCommand();
                WrapperConfig wc = new WrapperConfig(wkhtmltopdfCommand);
                Pdf pdf = new Pdf(wc);
                pdf.addParam(new Param("--enable-local-file-access"));
                pdf.addParam(new Param("--page-size", "A4"));
                pdf.addParam(new Param("--header-spacing", "3"));
                pdf.addParam(new Param("--footer-spacing", "6"));
                pdf.addParam(new Param("--header-html", headerHtml));
                pdf.addParam(new Param("--footer-html", footerHtml));
                pdf.addPageFromString(bodyHtml);
    
                pdf.saveAsDirect(outputFilePath);
    
                fos.flush();
                fos.close();
    
                return fileUploadService.uploadFile(outputFile);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                FileUtil.del(outputFile);
            }
        }
    
        private String generateBodyHtml(Map<String, Object> data) throws IOException, TemplateException {
            Template template = configuration.getTemplate("/custom/article/规章详情-PDF-Body.html");
            try (ByteArrayOutputStream fileOutputStream = new ByteArrayOutputStream();
                 Writer writer = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8)) {
                template.process(data, writer);
    
                return fileOutputStream.toString();
            }
        }
    

    总结

    1. 在生成pdf文件中的过程中发现,如果网页不标准,比如缺少标题title,会出错,导致生成空白的pdf文件。
    2. 字体问题:在linux服务器环境,需要提前安装相关的中文字体
    有服务器上运行fc-list命令查看安装的中文字体
    fc-list :lang=zh-cn
    

    相关文章

      网友评论

          本文标题:使用wkhtmltopdf和freemaker生成pdf

          本文链接:https://www.haomeiwen.com/subject/cohtrrtx.html