1.基于PhantomJs实现html输出图片
Java & PhantomJs 实现 html 输出图片
借助 phantomJs 来实现将 html 网页输出为图片
在 quick-media 的体系里,html 转图片其实有两套方案:一套是 markdown-plugin 里基于 xhtmlrenderer 的纯 Java 渲染,另一套就是本文要说的,把 html 丢给无头浏览器 phantomjs 渲染,然后截屏输出图片。
1. 为什么需要 phantomjs
先聊一下问题背景:我们希望实现的效果是「给一个网页地址,输出这个网页渲染之后的截图」。
纯 Java 的方案(xhtmlrenderer,也就是 markdown-plugin 中 Html2ImageWrapper 的实现思路)是自己解析 html、自己排版、自己绘制,好处是不依赖任何外部程序,坏处也很明显:
- 它本质上是一个简化的排版引擎,css 支持有限,复杂的样式(如 flex、部分 js 动态渲染的内容)基本玩不转
- html 里的 js 不会执行,页面上靠 ajax/js 拉取的内容拿不到
- 适合场景是「我们自己生成的、结构可控的 html」(比如 markdown 转出来的),而不是任意的线上网页
而 phantomjs 是一个基于 WebKit 内核的无界面浏览器,它和真正的浏览器一样加载页面、执行 js、应用 css 样式,然后我们可以让它对当前页面截屏。也就是说,凡是浏览器能正常显示的页面,phantomjs 都能把它截成图,这就把渲染的复杂度从 Java 侧转嫁给了浏览器内核。
两个方案的简单对比:
| 对比项 | xhtmlrenderer(markdown-plugin) | phantomjs(本文) |
|---|---|---|
| 依赖 | 纯 Java,无外部依赖 | 需要本机安装 phantomjs |
| js 支持 | 不执行 js | 完整执行 js |
| css 支持 | 简单样式 | WebKit 内核支持 |
| 使用场景 | 结构可控的 html(如 markdown 生成的) | 任意线上 url |
| 输出控制 | 宽高自适应、输出格式等都可配置 | 截图结果即浏览器显示效果,参数较少 |
所以如果你的需求是「markdown 转 html 再转图片」,用 markdown-plugin 那套就够了;如果是「把一个线上网页存成图片」,phantomjs 方案更合适。
2. 前提准备:安装 phantomjs
phantom-plugin 本身只是「调用方」,真正干活的是本机的 phantomjs 程序,所以第一步得把它装好。这里以 2.1.1 版本为例。
2.1 mac 系统
# 下载
wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-macosx.zip
# 解压后,将可执行文件放到 PATH 目录下
sudo cp phantomjs-2.1.1-macosx/bin/phantomjs /usr/local/bin
# 验证是否 ok
phantomjs --version
# 输出版本号,则表示 ok
2.2 linux 系统
# 下载
wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
# 解压
sudo su
tar -jxvf phantomjs-2.1.1-linux-x86_64.tar.bz2
# 如果解压报错,则先安装下面的
# yum -y install bzip2
# 简单点,移动到 bin 目录下
cp phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/local/bin
# 验证
phantomjs --version
2.3 windows 系统
老文档里写的是「就不要玩了,没啥意思」😂,不过 Windows 下其实也能跑:下载 phantomjs-2.1.1-windows.zip 解压,记住 bin\phantomjs.exe 的绝对路径(比如 D:/soft/phantomjs/bin/phantomjs.exe),后面通过 JVM 系统属性告诉 Java 进程即可(见下文 3.2 节),不强求配置环境变量。
如果希望通过 phantomjs --version 直接验证,则可以把 bin 目录加到系统的 Path 环境变量里。
2.4 linux 下的中文字体
如果是在 linux 服务器上用,且页面包含中文,请确保系统安装了中文字体,否则截图里的中文会变成一个个方框:
# centos
yum install -y fontconfig
yum install -y wqy-microhei-fonts wqy-zenhei-fonts
这是 phantomjs 系方案(包括后面会聊的 selenium 无头浏览器)的经典坑,渲染乱码十有八九是机器上没有字体。
3. 依赖引入
项目已发布到 maven 中央仓库,直接依赖即可
3.1 maven
<!-- https://mvnrepository.com/artifact/com.github.liuyueyi.media/phantom-plugin -->
<dependency>
<groupId>com.github.liuyueyi.media</groupId>
<artifactId>phantom-plugin</artifactId>
<!-- 以中央仓库最新版本为准 -->
<version>3.1.0</version>
</dependency>
内部依赖了 selenium-java(selenium 官方提供的 phantomjs driver)与 ghostdriver(phantomjs 的 webdriver 实现,来自 jitpack 仓库),如果你选择自己拉源码编译,需要留意 pom 里有 jitpack 的仓库配置。
3.2 指定 phantomjs 的可执行路径
插件是如何找到 phantomjs 的?看一眼源码:
private static String getPhantomJsPath() {
String path = System.getProperty("phantomjs.binary.path");
if (path == null || "".endsWith(path)) {
return "/usr/local/bin/phantomjs";
} else {
return path;
}
}
逻辑非常直白:
- 优先读取 JVM 系统属性
phantomjs.binary.path - 没有配置时,兜底使用
/usr/local/bin/phantomjs—— 这也是为什么上面推荐把可执行文件拷到/usr/local/bin下
所以两种姿势二选一:
- 把 phantomjs 放到
/usr/local/bin下(mac/linux 省事) - 启动时带上系统属性:
# 命令行方式
java -Dphantomjs.binary.path=/Users/yihui/soft/phantomjs/bin/phantomjs -jar xxx.jar
# windows 示例
java -Dphantomjs.binary.path=D:/soft/phantomjs/bin/phantomjs.exe -jar xxx.jar
代码内也可以在创建 driver 之前设置一次(注意插件是类加载时初始化的,最好在应用启动最前面做):
System.setProperty("phantomjs.binary.path", "D:/soft/phantomjs/bin/phantomjs.exe");
4. 使用姿势
这个插件对外暴露的 API 非常克制,整个插件就一个工具类 Html2ImageByJsWrapper,就一个静态方法:
/**
* 渲染 url 对应的网页,并输出截图
*/
public static BufferedImage renderHtml2Image(String url) throws IOException
以项目自带的测试 case 为例(Html2ImgTest#testRender):
@Test
public void testRender() {
BufferedImage img = null;
for (int i = 0; i < 20; ++i) {
String url = "https://my.oschina.net/u/566591/blog/1580020";
long start = System.currentTimeMillis();
img = Html2ImageByJsWrapper.renderHtml2Image(url);
long end = System.currentTimeMillis();
System.out.println("cost: " + (end - start));
}
}
传一个线上 url 进去,返回的就是渲染之后的 BufferedImage,拿到之后该怎么用就怎么用,比如直接落盘:
BufferedImage img = Html2ImageByJsWrapper.renderHtml2Image("https://blog.hhui.top");
ImageIO.write(img, "png", new File("/tmp/render.png"));
如果希望在前端网页中直接展示,可以配合 base-plugin 里的工具类,把图片转成 base64 的 data-uri 格式:
import com.github.hui.quick.plugin.base.Base64Util;
import com.github.hui.quick.plugin.base.DomUtil;
import com.github.hui.quick.plugin.base.constants.MediaType;
// 输出可以直接塞进 <img src="..."> 的 data-uri 字符串
System.out.println(DomUtil.toDomSrc(Base64Util.encode(img, "png"), MediaType.ImagePng));
5. 内部是怎么干的
虽然它只是个薄薄的封装,但把核心源码贴出来过一遍,有助于理解后面的一些坑:
public class Html2ImageByJsWrapper {
private static PhantomJSDriver webDriver;
static {
try {
webDriver = getPhantomJs();
} catch (Exception e) {
e.printStackTrace();
}
}
private static PhantomJSDriver getPhantomJs() {
//设置必要参数
DesiredCapabilities dcaps = new DesiredCapabilities();
//ssl证书支持
dcaps.setCapability("acceptSslCerts", true);
//截屏支持
dcaps.setCapability("takesScreenshot", true);
//css搜索支持
dcaps.setCapability("cssSelectorsEnabled", true);
//js支持
dcaps.setJavascriptEnabled(true);
//驱动支持(第二参数表明的是你的phantomjs引擎所在的路径,which/whereis phantomjs可以查看)
dcaps.setCapability(PhantomJSDriverService.PHANTOMJS_EXECUTABLE_PATH_PROPERTY, getPhantomJsPath());
//创建无界面浏览器对象
return new PhantomJSDriver(dcaps);
}
public static BufferedImage renderHtml2Image(String url) throws IOException {
if (webDriver != null) {
webDriver.get(url);
File file = webDriver.getScreenshotAs(OutputType.FILE);
return ImageIO.read(file);
} else {
return null;
}
}
}
几处值得注意的点:
- 静态块初始化:
PhantomJSDriver在类加载时就创建好了,phantomjs 进程随之启动,后续所有的渲染都复用这一个浏览器实例,避免每次截图都拉起进程的开销(测试 case 里循环 20 次渲染并计时,就是为了观察复用之后的耗时表现) - 参数说明:
DesiredCapabilities里开启了 ssl 证书、截屏、css 选择器、js 支持,都是截网页图的必要开关 - 渲染流程:
webDriver.get(url)会等页面加载完成,然后getScreenshotAs对当前窗口截屏,得到一个临时文件,再读成BufferedImage返回 - phantomjs 路径:通过
PHANTOMJS_EXECUTABLE_PATH_PROPERTY这个 capability 传给了 driver,取值规则见上文 3.2
6. 常见坑
6.1 找不到 phantomjs 可执行文件
报错一般是 phantomjs driver 初始化失败,栈里会有找不到 executable 之类的提示。排查顺序:
- 确认机器上真的装了 phantomjs:
which phantomjs(win 上是where phantomjs) - 确认路径和插件期望的对得上:没有配置
phantomjs.binary.path时,兜底路径是/usr/local/bin/phantomjs,只适用于 mac/linux - windows 用户必须显式配置
-Dphantomjs.binary.path=...,且注意路径分隔符用/或\\
6.2 中文乱码 / 中文变方框
表现是截图里的中文全是「豆腐块」。原因在前面提过:机器上没有中文字体,phantomjs 渲染时找不到字形。装上字体即可(见 2.4 节),linux 服务器上部署时尤其容易踩。
另外注意插件默认以 UTF-8 读取文件,如果你的 html 文件本身是 GBK 编码,也可能出现乱码,这类情况属于 html 源文件的问题,改文件编码即可。
6.3 截图只有一小块 / 长网页被截断
phantomjs 截屏截的是「当前视口」,默认窗口尺寸比较小(400x300),所以页面超长时会只截到第一屏。
这是方案本身的限制:Html2ImageByJsWrapper 没有暴露设置视口大小的参数。如果遇到这种场景,可以下载源码自己改造——PhantomJSDriver 实现了标准 WebDriver 接口,在 renderHtml2Image 里调用 webDriver.manage().window().setSize(new Dimension(w, h)) 即可放大视口;或者干脆按自己的需求 fork 一份 wrapper(整个类也就七十来行,改造成本很低)。
6.4 页面加载超时
webDriver.get(url) 是同步阻塞的,页面一直加载不完时,调用就会一直挂着。如果渲染的 url 来自外部输入,建议在外面套一层自己的超时控制(如异步线程 + 超时取消),不要让一次异常的页面加载拖垮整个服务。
网络不佳时,页面里引用的远程图片、css 加载失败,也会导致截图内容不完整,这一点在弱网环境下要有心理预期。
6.5 并发支持有限
插件内部只有一个静态的 webDriver 实例,renderHtml2Image 串行使用它是安全的,但多线程并发调用同一个无头浏览器实例并没有得到保证。如果业务上是高并发场景,稳妥的做法是自己基于 selenium + ghostdriver 搭建一个 phantomjs 实例池(或者每个线程一个实例),不要直接并发怼这个工具类。
6.6 时代局限
phantomjs 项目官方已经停止维护,其 WebKit 内核相对老旧,对新的 js 语法、新的 css 特性支持有限,遇到新潮的网页可能会渲染异常或直接加载失败。这是选型时需要心里有数的:phantomjs 方案胜在简单、有历史沉淀,但如果要求对新页面有良好的兼容性,可以考虑换成基于 chrome 的 headless 方案,思路完全一致(同样是 selenium 驱动 + 截屏),只是驱动换成了 ChromeDriver。
7. 小结
phantom-plugin 提供的是一个非常轻量的封装:Html2ImageByJsWrapper.renderHtml2Image(url) 一行调用,把「网页」变成「图片」。它和 markdown-plugin 的 xhtmlrenderer 方案互补——后者擅长渲染我们自己生成的 html,前者擅长截真实的线上网页。
使用它的三要素:
- 本机装好 phantomjs 2.1.1
- 保证 Java 进程能找到可执行文件(环境变量 或
phantomjs.binary.path) - 注意视口大小、字体、超时这几个经典坑
相关文档: