专栏首页小灰灰Java 实现 markdown转Image

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 条评论
登录 后参与评论

相关文章

  • 【SpringBoot WEB系列】异步请求知识点与使用姿势小结

    在 Servlet3.0 就引入了异步请求的支持,但是在实际的业务开发中,可能用过这个特性的童鞋并不多?

    一灰灰blog
  • java redis 通用组建

    前言 redis 是个干嘛的 ? 看官网:http://redis.io/ 一句话,这里redis当做缓存(或者本来就是), 利用java写一个jedis的读...

    一灰灰blog
  • 【WEB系列】静态资源配置与读取

    SpringWeb项目除了我们常见的返回json串之外,还可以直接返回静态资源(当然在现如今前后端分离比较普遍的情况下,不太常见了),一些简单的web项目中,前...

    一灰灰blog
  • 【带着canvas去流浪】 (3)绘制饼图

    使用原生canvasAPI绘制饼图(南丁格尔玫瑰)。(截图以及数据来自于百度Echarts官方示例库【查看示例链接】)。

    大史不说话
  • pytorch学习笔记(八):PytTorch可视化工具 visdom

    Visdom PyTorch可视化工具 本文翻译的时候把 略去了 Torch部分。 项目地址 ? 一个灵活的可视化工具,可用来对于 实时,富数据的 创建,组织和...

    ke1th
  • 用Java 8 stream流实现简洁的集合处理

    java 8已经发行好几年了,前段时间java 12也已经问世,但平时的工作中,很多项目的环境还停留在java1.7中。而且java8的很多新特性都是革命性的,...

    宜信技术学院
  • Unix Pipes to Javascript Pipes

    NodeJS中引入流概念来解决I/O异步问题,如果没有Stream,我们可能要这么写代码:

    IMWeb前端团队
  • Unix Pipes to Javascript Pipes

    本文作者:IMWeb 杨文坚 原文出处:IMWeb社区 未经同意,禁止转载 Unix Pipes Unix管道扫描稿 ? 简单样例: $ ne...

    IMWeb前端团队
  • 2019前端必用正则表达式汇总整理——亲自验证,请放心使用!

    用户1272076
  • HTTP/2 服务器推送(Server Push)

    HTTP/2 协议的主要目的是提高网页性能。 头信息(header)原来是直接传输文本,现在是压缩后传输。原来是同一个 TCP 连接里面,上一个回应(respo...

    wangxl

扫码关注云+社区

领取腾讯云代金券