Java 实现 markdown转Image

markdown 转 image

前段时间实现了长图文生成的基本功能,然后想了下能否有个进阶版,直接将markdown生成渲染后的图片呢?

思路

有不少的库可以将 markdown 转为 html,那么这个需求就可以转为 html转Image了

1. markdown 转 html

可以参看之前的博文《Java 实现 markdown转Html》

2. html 转 图片

主要的核心问题就在这里了,如何实现html转图片?

  • 直接实现html转图片的包没怎么见,看到一个 html2image, 还不太好用
  • 在 AWT or Swing 的Panel上显示网页,在把Panel输出为 image 文件
  • 使用js相关技术实现转换

本篇博文具体实现以 html2image 的实现逻辑作为参考,然后定制实现一把(后面有机会写一篇利用js来实现html转图片的博文)

html2image 的实现原理

html2image 基本上没啥维护了,内部主要是利用了 xhtmlrender 实现html渲染为图片

Graphics2DRenderer renderer = new Graphics2DRenderer();
// 设置渲染内容
renderer.setDocument(document, document.getDocumentURI());

// 获取Graphics2D
graphics2D = bufferedImage.createGraphics();
renderer.layout(graphics2D, dimension);

// 内容渲染
renderer.render(graphics2D);

说明

  1. 为什么并不直接使用 java-html2image ?
  • 因为有些定制的场景支持得不太友好,加上源码也比较简单,所以干脆站在前人的基础上进行拓展
  1. 设计目标(这里指html转图片的功能)
  • 生成图片的宽可指定
  • 支持对线上网页进行转图片
  • 支持对html中指定的区域进行转换
  • css样式渲染支持

实现

本篇先会先实现一个基本的功能,即读去markdown文档, 并转为一张图片

1. markdown 转 html 封装

利用之前封装的 MarkDown2HtmlWrapper 工具类

具体实现逻辑参考项目工程,和markdown转html博文

2. html 转 image

参数配置项 HtmlRenderOptions

注意

  • html 为 Document 属性
  • autoW, autoH 用于控制是否自适应html实际的长宽
@Data
public class HtmlRenderOptions {

    /**
     * 输出图片的宽
     */
    private Integer w;


    /**
     * 输出图片的高
     */
    private Integer h;


    /**
     * 是否自适应宽
     */
    private boolean autoW;


    /**
     * 是否自适应高
     */
    private boolean autoH;


    /**
     * 输出图片的格式
     */
    private String outType;

    /**
     * html相关内容
     */
    private Document document;
}

封装处理类

同样采用Builder模式来进行配置项设置

public class Html2ImageWrapper {

    private static DOMParser domParser;

    static {
        domParser = new DOMParser(new HTMLConfiguration());
        try {
            domParser.setProperty("http://cyberneko.org/html/properties/names/elems", "lower");
        } catch (Exception e) {
            throw new RuntimeException("Can't create HtmlParserImpl", e);
        }
    }


    private HtmlRenderOptions options;


    private Html2ImageWrapper(HtmlRenderOptions options) {
        this.options = options;
    }


    private static Document parseDocument(String content) throws Exception {
        domParser.parse(new InputSource(new StringReader(content)));
        return domParser.getDocument();
    }


    public static Builder of(String html) {
        return new Builder().setHtml(html);
    }


    public static Builder ofMd(MarkdownEntity entity) {
        return new Builder().setHtml(entity);
    }


    public BufferedImage asImage() {
        BufferedImage bf = HtmlRender.parseImage(options);
        return bf;
    }


    public boolean asFile(String absFileName) throws IOException {
        File file = new File(absFileName);
        FileUtil.mkDir(file);

        BufferedImage bufferedImage = asImage();
        if (!ImageIO.write(bufferedImage, options.getOutType(), file)) {
            throw new IOException("save image error!");
        }

        return true;
    }


    public String asString() throws IOException {
        BufferedImage img = asImage();
        return Base64Util.encode(img, options.getOutType());
    }


    @Getter
    public static class Builder {
        /**
         * 输出图片的宽
         */
        private Integer w = 600;

        /**
         * 输出图片的高度
         */
        private Integer h;

        /**
         * true,根据网页的实际宽渲染;
         * false, 则根据指定的宽进行渲染
         */
        private boolean autoW = true;

        /**
         * true,根据网页的实际高渲染;
         * false, 则根据指定的高进行渲染
         */
        private boolean autoH = false;


        /**
         * 输出图片的格式
         */
        private String outType = "jpg";


        /**
         * 待转换的html内容
         */
        private MarkdownEntity html;


        public Builder setW(Integer w) {
            this.w = w;
            return this;
        }

        public Builder setH(Integer h) {
            this.h = h;
            return this;
        }

        public Builder setAutoW(boolean autoW) {
            this.autoW = autoW;
            return this;
        }

        public Builder setAutoH(boolean autoH) {
            this.autoH = autoH;
            return this;
        }

        public Builder setOutType(String outType) {
            this.outType = outType;
            return this;
        }


        public Builder setHtml(String html) {
            this.html = new MarkdownEntity();
            return this;
        }


        public Builder setHtml(MarkdownEntity html) {
            this.html = html;
            return this;
        }

        public Html2ImageWrapper build() throws Exception {
            HtmlRenderOptions options = new HtmlRenderOptions();
            options.setW(w);
            options.setH(h);
            options.setAutoW(autoW);
            options.setAutoH(autoH);
            options.setOutType(outType);


            if (fontColor != null) {
                html.addDivStyle("style", "color:" + options.getFontColor());
            }
            html.addDivStyle("style", "width:" + w + ";");
            html.addWidthCss("img");
            html.addWidthCss("code");

            options.setDocument(parseDocument(html.toString()));

            return new Html2ImageWrapper(options);
        }

    }
}

上面的实现,有个需要注意的地方

如何将html格式的字符串,转为 Document 对象

利用了开源工具 nekohtml, 可以较好的实现html标签解析,看一下DOMParse 的初始化过程

private static DOMParser domParser;

static {
    domParser = new DOMParser(new HTMLConfiguration());
    try {
        domParser.setProperty("http://cyberneko.org/html/properties/names/elems", 
        "lower");
    } catch (Exception e) {
        throw new RuntimeException("Can't create HtmlParserImpl", e);
    }
}

try语句块中的内容并不能缺少,否则最终的样式会错乱,关于 nekohtml 的使用说明,可以查阅相关教程

上面的封装,主要是HtmlRenderOptions的构建,主要的渲染逻辑则在下面

渲染

利用 xhtmlrenderer 实现html的渲染

  • 宽高的自适应
  • 图片的布局,内容渲染
public class HtmlRender {

    /**
     * 输出图片
     *
     * @param options
     * @return
     */
    public static BufferedImage parseImage(HtmlRenderOptions options) {
        int width = options.getW();
        int height = options.getH() == null ? 1024 : options.getH();
        Graphics2DRenderer renderer = new Graphics2DRenderer();
        renderer.setDocument(options.getDocument(), options.getDocument().getDocumentURI());


        Dimension dimension = new Dimension(width, height);
        BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D graphics2D = GraphicUtil.getG2d(bufferedImage);

        // 自适应修改生成图片的宽高
        if (options.isAutoH() || options.getH() == null) {
            // do layout with temp buffer
            renderer.layout(graphics2D, new Dimension(width, height));
            graphics2D.dispose();

            Rectangle size = renderer.getMinimumSize();
            final int autoWidth = options.isAutoW() ? (int) size.getWidth() : width;
            final int autoHeight = (int) size.getHeight();
            bufferedImage = new BufferedImage(autoWidth, autoHeight, BufferedImage.TYPE_INT_RGB);
            dimension = new Dimension(autoWidth, autoHeight);

            graphics2D = GraphicUtil.getG2d(bufferedImage);
        }


        renderer.layout(graphics2D, dimension);
        renderer.render(graphics2D);
        graphics2D.dispose();
        return bufferedImage;
    }
}

测试

@Test
public void testParse() throws Exception {
    String file = "md/tutorial.md";

    MarkdownEntity html = MarkDown2HtmlWrapper.ofFile(file);

    BufferedImage img = Html2ImageWrapper.ofMd(html)
            .setW(600)
            .setAutoW(false)
            .setAutoH(true)
            .setOutType("jpg")
            .build()
            .asImage();

    ImageIO.write(img, "jpg", new File("/Users/yihui/Desktop/md.jpg"));
}

输出图片

然后演示一个对项目中实际的教程文档输出图片的动态示意图, 因为生成的图片特别特别长,所以就不贴输出的图片了,有兴趣的同学可以下载工程,实际跑一下看看

源markdown文件地址:

https://github.com/liuyueyi/quick-media/blob/master/doc/images/imgGenV2.md

其他

相关博文 : Java 实现 markdown转Html

项目地址:https://github.com/liuyueyi/quick-media

个人博客:一灰的个人博客

参考博文

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏一直在跳坑然后爬坑

Flutter “跳转页面”(二)前言正文

写了这么多文章,有翻译文档的,有自己理解的,也不知道到底是怎么样的风格更能让人接受,希望大家能给点意见或建议。

46220
来自专栏转载gongluck的CSDN博客

vc++快速使用richedit控件

? vc++快速使用richedit控件 1)初始化//必须加,否则无法显示窗口 CXXXApp::CXXXApp()  //找到应用类 { // TOD...

45480
来自专栏hotqin888的专栏

bootstrap table x-editable select2——带图标的选择

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hotqin888/article/det...

1.4K10
来自专栏玄魂工作室

如何将HTML字符转换为DOM节点并动态添加到文档中

将字符串动态转换为DOM节点,在开发中经常遇到,尤其在模板引擎中更是不可或缺的技术。 字符串转换为DOM节点本身并不难,本篇文章主要涉及两个主题:<br />

18620
来自专栏DOTNET

学会WCF之试错法——超时

服务契约 [ServiceContract] public interface IService { [OperationCon...

33460
来自专栏進无尽的文章

编码篇-OC跨多层UI事件传递处理

在 iOS 中,对象间的交互模式大概有这几种:直接 property 传值、delegate、KVO、block、protocol、多态、Target-Acti...

18130
来自专栏WindCoder

Angular6自定义表单控件方式集成Editormd

曾经找到过“Editor.md”,看之心喜,一直想在Angular中集成下这款markdownpad编辑器玩,在网上也只找到一篇通过指令集成的,虽然可以实现,但...

55320
来自专栏木宛城主

ASP.NET MVC使用Bootstrap系列(5)——创建ASP.NET MVC Bootstrap Helpers

序言 ASP.NET MVC允许开发者创建自定义的HTML Helpers,不管是使用静态方法还是扩展方法。一个HTML Helper本质上其实是输出一段HT...

26080
来自专栏技术博客

JavaScript开发中几个常用知识点总结

  最近在做项目的时候自己写了一些JavaScipt代码,于是自己又进行简单的查阅资料整理了一下,发现了如下几个比较有用的知识点:

10240
来自专栏XAI

ZeroClipboard实现多个浏览器兼容的复制文本到剪贴板的功能

ZeroClipboard实现多个浏览器兼容的复制文本到剪贴板的功能 本人在项目中使用的js版本。为了方便大家下载。直接粘贴代码给大家看。版本是1.2.0 /*...

28470

扫码关注云+社区

领取腾讯云代金券