poi-tl是如何将富文本渲染到Word文件

举报
KevinQ 发表于 2024/10/17 17:58:27 2024/10/17
【摘要】 poi-tl在工作中,遇到动态渲染word模板的需求,了解并使用了poi-tl。是一个基于Apacha POI的Word模板引擎。官方文档上有一句很漂亮的话:在文档的任何地方做任何事情(Do Anything Anywhere)是poi-tl的星辰大海。poi-tl支持在word文件中动态的插入文字、图片、表格、列表、区块对、嵌套等。因为在工作中,遇到一种比较复杂的情况是富文本渲染到Wor...

poi-tl

在工作中,遇到动态渲染word模板的需求,了解并使用了poi-tl。是一个基于Apacha POI的Word模板引擎。
官方文档上有一句很漂亮的话:

在文档的任何地方做任何事情(Do Anything Anywhere)是poi-tl的星辰大海。

poi-tl支持在word文件中动态的插入文字、图片、表格、列表、区块对、嵌套等。

因为在工作中,遇到一种比较复杂的情况是富文本渲染到Word文件中,因此对于这种poi-tl如何实现这部分比较感兴趣,尝试来学习一下。

HtmlRenderPolicy

根据文档介绍,poi-tl富文本渲染需要依赖poi-tl-ext中的HtmlRenderPolicy

我们先来看下纯文字情况下的渲染效果(不使用HtmlRenderPolicy):

原模板如下:

![[Pasted image 20241017163606.png]]
准备在标签{{my_html}}位置插入html内容。html富文本源代码如下:

<div>
    <h1>大标题</h1>

	    <h2>标题1</h2>

		    <p>内容111</p>

	    <h2>标题2</h2>

		    <p>内容222</p>

</div>

在网页上展示形式如下图:
![[Pasted image 20241017163724.png]]

我们来看poi-tl如何实现内容插入

实现代码

/**
     * @param templatePath 模板文件
     * @param htmlFilePath html文件
     * @param outputPath 输出文件
     * @throws IOException
     */
    public static void insertHtmlToDocx(String templatePath, String htmlFilePath, String outputPath) throws IOException {
        // 读取html文件
        String htmlContent = FileUtils.readFileToString(new File(htmlFilePath), "utf-8");
        // 读取模板
        XWPFTemplate template = XWPFTemplate.compile(templatePath);
        // 构造poi-tl需要的数据map
        Map<String, Object> dataMap = new HashMap<>();
        dataMap.put("my_html", htmlContent);
        template.render(dataMap);
        template.writeAndClose(new FileOutputStream(outputPath));
    }

(未使用HtmlRenderPolicy)渲染效果:
![[Pasted image 20241017170744.png]]

再来看下使用HtmlRenderPolicy的代码与效果:

/**
     * @param templatePath 模板文件
     * @param htmlFilePath html文件
     * @param outputPath 输出文件
     * @throws IOException
     */
    public static void insertHtmlToDocx(String templatePath, String htmlFilePath, String outputPath) throws IOException {
        // 读取html文件
        String htmlContent = FileUtils.readFileToString(new File(htmlFilePath), "utf-8");

        Configure config = Configure.builder()
                .bind("my_html", new HtmlRenderPolicy()).build();
        // 读取模板
        XWPFTemplate template = XWPFTemplate.compile(templatePath, config);

        // 构造poi-tl需要的数据map
        Map<String, Object> dataMap = new HashMap<>();
        dataMap.put("my_html", htmlContent);
        template.render(dataMap);
        template.writeAndClose(new FileOutputStream(outputPath));
    }

渲染效果如下图:
![[Pasted image 20241017171401.png]]

代码解析:

在HtmlRenderPolicy.java中,存在主要三个方法:构造函数,doRenderafterRender(其他方法我们暂时忽略):

package org.ddr.poi.html;

public class HtmlRenderPolicy extends AbstractRenderPolicy<String> {
    private final Map<String, ElementRenderer> elRenderers;
    private final HtmlRenderConfig config;

    public HtmlRenderPolicy() {
        this(new HtmlRenderConfig());
    }

    /** @deprecated */
    @Deprecated
    public HtmlRenderPolicy(String globalFont, CSSLength globalFontSize) {
        this(new HtmlRenderConfig());
        this.config.setGlobalFont(globalFont);
        this.config.setGlobalFontSize(globalFontSize);
    }

    public HtmlRenderPolicy(HtmlRenderConfig config) {
	    // 
        ElementRenderer[] renderers = new ElementRenderer[]{new ARenderer(), new BigRenderer(), new BoldRenderer(), new BreakRenderer(), new DeleteRenderer(), new FigureRenderer(), new FigureCaptionRenderer(), new HeaderBreakRenderer(), new HeaderRenderer(), new ImageRenderer(), new ItalicRenderer(), new LaTeXRenderer(), new ListItemRenderer(), new ListRenderer(), new MarkRenderer(), new MathRenderer(), new OmittedRenderer(), new PreRenderer(), new RubyRenderer(), new SmallRenderer(), new SubscriptRenderer(), new SuperscriptRenderer(), new SvgRenderer(), new TableCellRenderer(), new TableRenderer(), new UnderlineRenderer(), new WalkThroughRenderer()};
        // 省略代码
    }

    public void doRender(RenderContext<String> context) throws Exception {
        // 渲染
    }

    protected void afterRender(RenderContext<String> context) {
        // 渲染后处理

    }

}

  1. 在构造方法中,构造了关于Html中各个组件的映射处理类。比如<a>对应ARenderer.java<b>加粗标签对应BoldRenderer.java<br>换行标签对应BreakRenderer.java等等,对与Html中可能出现的标签都做了一一对应,甚至还包括表格、列表、图片的对应渲染器。

这些渲染器都实现了ElementRenderer渲染器,该渲染器的代码如下:

/**
 * HTML元素渲染器
 *
 * @author Draco
 * @since 2021-02-08
 */
public interface ElementRenderer {
    /**
     * 开始渲染
     *
     * @param element HTML元素
     * @param context 渲染上下文
     * @return 是否继续渲染子元素
     */
    boolean renderStart(Element element, HtmlRenderContext context);

    /**
     * 元素渲染结束需要执行的逻辑
     *
     * @param element HTML元素
     * @param context 渲染上下文
     */
    default void renderEnd(Element element, HtmlRenderContext context) {
    }

    /**
     * @return 支持的HTML标签
     */
    String[] supportedTags();

    /**
     * @return 是否为块状渲染,如果为true在Word中会另起一个Paragraph
     */
    boolean renderAsBlock();
}
  1. 在doRender方法代码如下:
@Override
    public void doRender(RenderContext<String> context) throws Exception {
        Document document = JsoupUtils.parse(context.getData());
        document.outputSettings().prettyPrint(false).indentAmount(0);

		// 构造HTML渲染上下文
        HtmlRenderContext htmlRenderContext = new HtmlRenderContext(context, elRenderers::get);
        htmlRenderContext.setGlobalFont(config.getGlobalFont());
        if (config.getGlobalFontSizeInHalfPoints() > 0) {
            htmlRenderContext.setGlobalFontSize(BigInteger.valueOf(config.getGlobalFontSizeInHalfPoints()));
        }
        htmlRenderContext.getNumberingContext().setIndent(config.getNumberingIndent());
        htmlRenderContext.getNumberingContext().setHanging(config.getNumberingHanging());
        htmlRenderContext.getNumberingContext().setSpacing(config.getNumberingSpacing());
        htmlRenderContext.setShowDefaultTableBorderInTableCell(config.isShowDefaultTableBorderInTableCell());

        htmlRenderContext.renderDocument(document);
    }

其中,比较复杂的类为html渲染上下文HtmlRenderContext,其中实现了包括样式栈、父容器栈、字号栈、列表上下文等较为复杂的功能。
通过HtmlRenderContext.renderDocument方法逐级实现html中各个节点的渲染,其代码如下:

public void renderDocument(Document document) {
        Element body = document.body();
        Element html = body.parent();
        if (html.hasAttr(HtmlConstants.ATTR_STYLE)) {
            pushInlineStyle(getCssStyleDeclaration(html), html.isBlock());
        }
        if (body.hasAttr(HtmlConstants.ATTR_STYLE)) {
            pushInlineStyle(getCssStyleDeclaration(body), body.isBlock());
        }
        for (Node node : body.childNodes()) {
            renderNode(node);
        }
    }

    public void renderNode(Node node) {
        boolean isElement = node instanceof Element;

        if (isElement) {
            Element element = ((Element) node);
            renderElement(element);
        } else if (node instanceof TextNode) {
            renderText(((TextNode) node).getWholeText());
        }
    }
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。