关于我用xhtmlrenderer将html转换img结果样式飞了的这档事

项目有个业务需求,就是客户需要我们提供一个可以导出企业各项数据的word平台,数据是从企通查和天眼查查过来的,其中有个表是企业的发展历程数据,需要我们将各大模块数据从第三方查询出来然后导出word

根据我们课题研究,最后决定用Freemarker模板引擎画发展历程页面(要图片显示的),然后将发展历程的ftl文件转化成html再转成img,然后将发展历程的img转成base64再引入进word页面的ftl最后导出word。这样word里不但有图片也有自己画的表格。然后转图片这块出现了问题

由于项目原因不能公开页面截图了

首先在网上找解决办法,大多数都是用的xhtmlrenderer包

这个包我亲测了,现在是2022年,我试了,很多样式直接导出来不显示,尤其是CSS3的,像我们页面用的flex啥的压根样式都不对,所以这就有问题了,毕竟工作还得给人家弄是么

我去maven远程仓库查了下jar包

可以看到这个jar包已经有十年没升级了,已经不维护了

不过他还有另外一个版本

这个flying-saucer倒是还在维护,不过也根本不好使,border-radius该不好使还不好使。

后来看到CSDN有个老哥同样也遇到个这个问题 https://blog.csdn.net/xcc_2269861428/article/details/85246815

他想实现的功能跟我们差不多,我就试了一下,果真好用,而且还简单

工具下载地址

https://wkhtmltopdf.org/downloads.html

最后附上我们的转换代码

application.yml

# FTL转ImageBase54
ftlToImg:
  resourcePath: "templates"
  modelName: "companyInfo.ftl"
  inputFileName: "E:/JianBao/"
  wkhtmltopdf: "F:/wkhtmltopdf/bin/wkhtmltoimage.exe"

# WORD导出目录
create:
  report_createFilePath_50_folder: E://JianBao/downloadPath/

spring.freemarker.template-loader-path=classpath:/templates/

FtlToHtmlToImg

package com.cei.utils;

import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import sun.misc.BASE64Encoder;

import javax.annotation.PostConstruct;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.List;

@Component
public class FtlToHtmlToImg {

    private static FtlToHtmlToImg staticInstance = new FtlToHtmlToImg();
    // 资源绝对路径
    @Value("${ftlToImg.resourcePath}")
    private String resourcePath;
    // 文件ftl模板名称
    @Value("${ftlToImg.modelName}")
    private String modelName;
    // 文件输出路径
    @Value("${ftlToImg.inputFileName}")
    private String inputFileName;
    // wkhtmltopdf平台路径(安装本地的绝对路径,到bin里面的exe)
    @Value("${ftlToImg.wkhtmltopdf}")
    private String wkhtmltopdf;
    @Autowired
    FreeMarkerConfigurer freeMarkerConfigurer;

    @PostConstruct
    public void init() {
        staticInstance.resourcePath = resourcePath;
        staticInstance.modelName = modelName;
        staticInstance.inputFileName = inputFileName;
        staticInstance.wkhtmltopdf = wkhtmltopdf;
        staticInstance.freeMarkerConfigurer = freeMarkerConfigurer;
    }

    /**
     * 获取模板转img
     *
     * @param
     * @return
     */
    public List<Map<String, Object>> createImgBase64(List<Map<String, Object>> dataList, BigInteger companyId, String companyName) {
        File file = new File(staticInstance.inputFileName + companyId + ".html");
        // 如果文件夹不存在则创建文件夹
        if (!file.exists() && !file.isDirectory()) {
            //创建上级目录
            file.getParentFile().mkdirs();
        }
        //ftl转html
        String ftlFile = staticInstance.modelName;
        String html = null;
        try {
            html = ftlToString(dataList, ftlFile, companyId, companyName);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (TemplateException e) {
            throw new RuntimeException(e);
        }
        // html转图片转base64
        List<Map<String, Object>> ImageBase64 = new ArrayList<>();
        try {
            ImageBase64 = ImageRender(html, companyId, companyName);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return ImageBase64;
    }

    /**
     * @param map          需要填充的数据集合
     * @param templateName 被填充的ftl文件
     * @return html数据
     * @throws IOException
     * @throws TemplateException
     * @Description: 将ftl文件转html文件
     * @Author:
     * @Date:
     */
    public String ftlToString(List<Map<String, Object>> dataList, String templateName, BigInteger companyId, String companyName) throws IOException, TemplateException {
        LoggerUtil.info("开始转换企业ID【" + companyId + "】【" + companyName + "】的页面, ftl转html");
        String value = "";
//        Configuration configuration = new Configuration();
//        Resource resource = new ClassPathResource(staticInstance.resourcePath);
//        File sourceFile = resource.getFile();
//        String ftlPath = sourceFile.getAbsolutePath();
        String filName = templateName;
        String encoding = "UTF-8";
        StringWriter out = new StringWriter();
//        configuration.setDirectoryForTemplateLoading(new File(ftlPath));
        Template template = staticInstance.freeMarkerConfigurer.getConfiguration().getTemplate(filName, encoding);
        template.setEncoding(encoding);
        Map<String, Object> map = new HashMap<>();
        map.put("historyList", dataList);
        template.process(map, out);
        out.flush();
        out.close();
        value = out.getBuffer().toString();
        LoggerUtil.info("结束转换企业ID【" + companyId + "】【" + companyName + "】的页面, ftl转html");
        return value;
    }

    /**
     * @param html html代码
     * @return base64图片
     * @throws IOException
     * @Description: 将html转换成图片并切割
     * @Author:
     * @Date:
     */
    public List<Map<String, Object>> ImageRender(String html, BigInteger companyId, String companyName) throws IOException {
        LoggerUtil.info("开始输出企业ID【" + companyId + "】【" + companyName + "】的html页面临时文件");
        // 将html输出为文件
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(staticInstance.inputFileName + companyId + ".html"), StandardCharsets.UTF_8));
        bufferedWriter.write(html);
        bufferedWriter.newLine();
        File source = new File(staticInstance.inputFileName + companyId + ".html");
        bufferedWriter.flush();
        bufferedWriter.close();
        LoggerUtil.info("结束输出企业ID【" + companyId + "】【" + companyName + "】的html页面临时文件");
        // 将输出的html文件转化为图片
        ProcessBuilder pb = new ProcessBuilder(staticInstance.wkhtmltopdf, staticInstance.inputFileName + companyId + ".html", staticInstance.inputFileName + companyId + ".png");
        Process process;
        try {
            LoggerUtil.info("开始转换企业ID【" + companyId + "】【" + companyName + "】的页面,html转img");
            process = pb.start();
            //注意,调用process.getErrorStream()而不是process.getInputStream()
            BufferedReader errStreamReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            String line = null;
            line = errStreamReader.readLine();
            while (line != null) {
                line = errStreamReader.readLine();
            }
            errStreamReader.close();
            process.destroy();
            LoggerUtil.info("结束转换企业ID【" + companyId + "】【" + companyName + "】的页面,html转img");
            LoggerUtil.info("开始切割企业ID【" + companyId + "】【" + companyName + "】的img,然后分别转换base64");
            // 读入大图
            File file = new File(staticInstance.inputFileName + companyId + ".png");
            FileInputStream fis = new FileInputStream(file);
            BufferedImage image = ImageIO.read(fis);
            fis.close();
            // 如果图片太大将切割图片
            // word里一页需要图片多高
            // 平均高度1726是算出来的,在word里一页大小是 height:697.4pt  width:413.7pt
            // 工具生成的图片宽度是1024,所以缩放最终得的结果是word里一页高度需要图片高度1726
            int picHeight = 1726;
            int picHeightTemp = 0;
            // 计算每个小图的宽度和高度
            int chunkWidth = image.getWidth();
            int chunkHeight = picHeight;
            int rows = 1;
            // 图片竖着分几块
            if (image != null && image.getHeight() > 1726) {
                rows = (image.getHeight() / 1726) + 1;
                picHeightTemp = image.getHeight() % 1726;
            } else {
                picHeightTemp = image.getHeight();
            }
            // 图片被切割成几块
            int chunks = rows;
            //大图中的一部分
            int count = 0;
            BufferedImage imgs[] = new BufferedImage[chunks];
            for (int x = 0; x < rows; x++) {
                // if判断,最后一张如果固定写死高度,会导致图片有上一张图内容拼接
                // 所以最后一张切割的高度,需要取余判断图片按平均分配高度之后,最后一张是剩余了多高
                if ((x + 1) == rows) {
                    //设置小图的大小和类型
                    imgs[count] = new BufferedImage(chunkWidth, picHeightTemp, image.getType());
                    //写入图像内容
                    // drawImage的参数,自己点进去源码里看官方注释
                    Graphics2D gr = imgs[count++].createGraphics();
                    gr.drawImage(image, 0, 0, chunkWidth, picHeightTemp, 0, chunkHeight * x, chunkWidth, image.getHeight(), null);
                    gr.dispose();
                } else {
                    imgs[count] = new BufferedImage(chunkWidth, chunkHeight, image.getType());
                    Graphics2D gr = imgs[count++].createGraphics();
                    gr.drawImage(image, 0, 0, chunkWidth, chunkHeight, 0, chunkHeight * x, chunkWidth, chunkHeight * x + chunkHeight, null);
                    gr.dispose();
                }
            }
            // 图片被切割的base64
            List<Map<String, Object>> base64List = new ArrayList<>();
            // 输出小图
            for (int i = 0; i < imgs.length; i++) {
                //ImageIO.write(imgs[i], "jpg", new File("C:\\img\\split\\img" + i + ".jpg"));
//                ImageIO.write(imgs[i], "png", new File("C:\\Users\\87151\\Desktop\\ceshi\\" + i + ".png"));
                Map<String, Object> map = new HashMap<>();
                // 图片base64
                map.put("url", imageToBase64(imgs[i]));
                // 图片文件名,不能重复,要不word生成的图片也重复
                map.put("name", 10000 + i);
                // 动态高度,防止最后一张拉伸
                if (i + 1 == imgs.length) {
                    map.put("myheight", picHeightTemp / 2.475);
                } else {
                    map.put("myheight", "697.4");
                }
                base64List.add(map);
            }
            LoggerUtil.info("结束切割企业ID【" + companyId + "】【" + companyName + "】的img,然后分别转换base64");
            // 删除临时文件缓存
            deleteTempFile(companyId);
            return base64List;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new ArrayList<>();
    }

    /**
     * 文件BufferedImage类型转BASE64
     *
     * @param bufferedImage
     * @return
     */
    public String imageToBase64(BufferedImage bufferedImage) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();//io流
        try {
            ImageIO.write(bufferedImage, "png", baos);//写入流中
            baos.flush();
            baos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        byte[] bytes = baos.toByteArray();//转换成字节
        BASE64Encoder encoder = new BASE64Encoder();
        String png_base64 = encoder.encodeBuffer(bytes).trim();//转换成base64串
        png_base64 = png_base64.replaceAll("\n", "").replaceAll("\r", "");//删除 \r\n
        return png_base64;
    }

    /**
     * 删除临时文件缓存
     */
    public void deleteTempFile(BigInteger companyId) {
        LoggerUtil.info("开始清除企业ID【" + companyId + "】的临时缓存文件成功");
        // 读取临时文件
        File file1 = new File(staticInstance.inputFileName + companyId + ".html");
        File file2 = new File(staticInstance.inputFileName + companyId + ".png");
        // 删除文件
        if (file1 != null) {
            file1.delete();
        }
        if (file2 != null) {
            file2.delete();
        }
        LoggerUtil.info("结束清除企业ID【" + companyId + "】的临时缓存文件成功");
    }
}

需要调用的类

// 发展历程数据写进FTL,再把FTL转成HTML,再把HTML转成IMG,把IMG转成BASE64返回来
FtlToHtmlToImg ftlToHtmlToImg = new FtlToHtmlToImg();
List<Map<String, Object>> devolopBase64List = ftlToHtmlToImg.createImgBase64(devolopEsResult, companyId, companyName);

        注意我们这边的参数,如果扒用的话需要酌情修改,我们参数是1.数据参数 2.企业id 3.企业名,企业ID和企业名字单纯就是为了传过去打印Log4j日志的,工具类里面的LoggerUtil是我们自己封装的Log4j的类,自己删了就行。

        工具类里面的方法大多数都是直接网上找的,不过找的帖子不是一个,太多没法发转发链接了,部分我们自己业务后补的是我自己写的。

spring.freemarker.template-loader-path=classpath:/templates/ 

 这句配置对应工具类里的

@Autowired
FreeMarkerConfigurer freeMarkerConfigurer;

        必加,因为如果单纯的用 new ClassPathResource 的话打包成jar包运行会获取不到resource的资源

        这里可以参考我另一个文章 https://blog.csdn.net/qq_37241221/article/details/126048613

 

        导出word的话,里面图片太大超过了word一页内容,word可能要么压缩图片高度,要么就直接后面内容不显示了,我这里自己写了个切割方案,会根据图片的高度自己判断该不该切割,然后导入到word,1726那个数字是我根据word一页多高自己算的,亲测好用

最后附上切割效果图 

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
### 回答1: 好的,以下是一个使用iText将HTML转换为PDF的帮助类的示例代码: ``` import com.itextpdf.text.Document; import com.itextpdf.text.DocumentException; import com.itextpdf.text.PageSize; import com.itextpdf.text.html.simpleparser.HTMLWorker; import com.itextpdf.text.pdf.PdfWriter; import java.io.*; import java.util.List; public class HtmlToPdfConverter { public static void convert(String htmlFilePath, String pdfFilePath) throws IOException, DocumentException { Document document = new Document(PageSize.A4); PdfWriter.getInstance(document, new FileOutputStream(pdfFilePath)); document.open(); HTMLWorker htmlWorker = new HTMLWorker(document); FileInputStream fileInputStream = new FileInputStream(htmlFilePath); InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream); List<?> htmlList = htmlWorker.parseToList(inputStreamReader, null); for (Object element : htmlList) { document.add((com.itextpdf.text.Element) element); } document.close(); } } ``` 在这个帮助类中,我们使用了iText库的`Document`、`PdfWriter`和`HTMLWorker`类来将HTML转换为PDF。该类有一个名为`convert`的静态方法,该方法接受两个参数:HTML文件的路径和生成PDF文件的路径。在方法中,我们首先创建一个A4大小的`Document`对象,然后使用`PdfWriter`将其与输出文件流关联起来。接着,我们创建一个`HTMLWorker`对象,然后使用`FileInputStream`和`InputStreamReader`将HTML文件读取为`List`类型的对象,最后通过循环将其添加到`Document`中。最后,我们调用`Document`的`close`方法来关闭生成的PDF文件。 请注意,此示例代码仅用于演示目的,并不是生产环境下完整的代码,您需要根据您的实际需求进行调整和扩展。 ### 回答2: 帮助类的设计思路如下: 首先,我们需要引入iText库,将其添加到项目的依赖中。 接下来定义一个名为HtmlToPdfHelper的类,该类包含一个convert方法,用于将HTML文件转换为PDF文件。该方法需要接收两个参数:htmlFilePath(表示待转换HTML文件路径)和pdfFilePath(表示生成的PDF文件路径)。 在convert方法内部,我们需要进行以下操作: 1. 创建一个Document对象,并设置页面大小和页边距。 2. 创建一个PdfWriter对象,将Document对象与pdfFilePath绑定。 3. 使用ITextRenderer类创建一个ITextRenderer对象,并将htmlFilePath传入其构造函数。 4. 调用ITextRenderer对象的layout方法,将HTML文件布局。 5. 调用ITextRenderer对象的createPDF方法,将布局后的文件保存到Document对象中。 6. 关闭Document对象和PdfWriter对象,完成PDF文件的生成。 下面是完整的HtmlToPdfHelper类的代码示例: ```java import com.lowagie.text.Document; import com.lowagie.text.PageSize; import com.lowagie.text.pdf.PdfWriter; import org.xhtmlrenderer.pdf.ITextRenderer; import java.io.FileOutputStream; public class HtmlToPdfHelper { public static void convert(String htmlFilePath, String pdfFilePath) throws Exception { Document document = new Document(PageSize.A4); PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(pdfFilePath)); document.open(); ITextRenderer renderer = new ITextRenderer(); renderer.setDocument(htmlFilePath); renderer.layout(); renderer.createPDF(writer.getDirectContent(), writer.getPageSize()); document.close(); writer.close(); } } ``` 使用上述的帮助类,我们可以将HTML文件转换为PDF文件,只需调用convert方法,并传入合适的参数即可。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值