GO | Markdown转图片/截图网页

Situation:

在进行企业微信推送的时候, 遇到了markdown格式消息推送长度超限的问题, 且业务上不允许丢失通知样式, 需要找到一种既能保持样式, 又能突破消息长度限制的推送方案

Task

1 . 方案制定

最先想到的是将Markdown内的详情内容转为图片形式, 基础信息部分保留原Markdown推送, 但是在我们的场景下遇到两个问题:

· 图片存储有网络域隔离, 推送出来的链接无法在外部网络环境访问

· 即使是使用图片, 由于使用了文字样式, 稍长的基础消息也有可能触发长度限制

因此这个方案最终没有采纳, 转而考虑整体转图片, 使用图片消息推送的方案

图片消息推送方案中, 我们遇到的问题是无法很好的处理原Markdown消息中的URL部分, 业务强依赖消息中的超链接来提升效率

最终, 选择了分步骤推送的方式来解决纯图片消息无法携带链接的问题

先推送一个包含全文的图片, 再推送一次概览Markdown提供链接

2. 技术实现

首先 直接将原来的Markdown进行图片渲染是行不通的, 没找到合适的渲染引擎, 最后选择了先转Html再通过浏览器渲染截图的方案

Action

需要两个包

1 . github.com/yuin/goldmark

goldmark是go的一个Markdown转Html库, 其实现了Markdown标准,(CommonMark) 易于拓展

2. github.com/chromedp/chromedp

chromedp是一个自动化浏览器测试的库, 基于Chrome DevTools Protocol(CDP)开发, 通过CDP可以直接调用Chromium

具体步骤见如下代码

// 先定义Markdown文本
md := `
> markdown document.

- Item 1
- Item 2
- [Link](https://baidu.com)
`

// 再定义基础的Html模板
htmlTemplate := `
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    <!-- 在这里可以设置自定义的HTML样式, 比如字体颜色之类的 -->
  </style>
</head>
<body>%s</body>
</html>
`

// 先把Markdown转HTML
var buf bytes.Buffer
if err := goldmark.Convert([]byte(md), &buf); err != nil {
   log.Fatal(err)
}
htmlContent := buf.String()

// 模板替换
fullHTML := fmt.Sprintf(htmlTemplate, htmlContent)

// 要将转换完的HTML内容做一次url转换
// 这和后面使用<strong>Chromium</strong>进行截图有关, 直接在地址栏使用
// ”data:text/html,“+HTML代码
// 的方式就可以渲染页面, 不需要存文件转一次了
// 如果不转url的话内容是现实不出来的
encodedHTML := url.PathEscape(fullHTML)

// 设置你<strong>Chromium</strong>的路径, 如果你装了Chrome那应该不用手动设置, Env里有
// 我这里用的是Edge, 所以要设置下
// 建议还是手动设置下, 因为最后都要跑到生产机器上去的, <strong>Chromium</strong>地址不确定
pathOpts := chromedp.ExecPath("/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge")

// 设置无头模式(headless mode)
// 这个是<strong>Chromium</strong>提供的一个模式, 可以在不显示GUI的情况下进行自动化操作
headlessOpts := chromedp.Flag("headless", true)

// 先设置执行选项
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), pathOpts, headlessOpts)

defer cancel()

// 连接<strong>Chromium</strong>
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()

// 这里开始截图
var screenshot []byte
err := chromedp.Run(ctx,
        // html的内容就在这设置
    chromedp.Navigate("data:text/html,"+encodedHTML),
        // 等待Body加载完毕
    chromedp.WaitVisible("body", chromedp.ByQuery),
        // 这里使用FullScreenshot API进行截图
        //如果你只需要部分区域, 可以使用CaptureScreenshot加设置Viewport大小的方式限制范围, 100是截图质量
    chromedp.FullScreenshot(&screenshot, 100),
)
if err != nil {
    log.Fatal(err)
}

// 保存图片
if err := os.WriteFile("output.png", screenshot, 0644); err != nil {
    log.Fatal(err)
}

Rusult

通过转换图片的方式, 我们解决了业务方的需求

从我们来看, 虽然引入Chromium的方式可能不太优雅, 但是这个方案可能是当前比较好的复杂样式markdown转图片的解决方案了