Java图片处理:网页转图片(HtmlToImage)
Java图片处理:网页转图片
需求来源于前端同事跟我反馈整天调试布局样式很难受,希望能有服务端网页转图片的方法。 记录一下研究过程。
- 可选方案及评价。
- 最终选取的方案
- 代码细节
- 不足之处
可选方案及评价
从以下三个方面考虑:
- 页面效果还原程度
- 是否支持复杂html/js解析
- 中文字体显示效果
JEditorPane
首先找到的方案是使用java内置的Html解释工具javax.swing.JEditorPane。
public void testHtml2Image() throws Exception
{
String html = "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>贝叶斯统计推断</title></head><body><article class=\"markdown-body\"><h1id=\"gps\"><a name=\"user-content-gps\"href=\"#gps\"class=\"headeranchor-link\"aria-hidden=\"true\"><spanclass=\"headeranchor\"></span></a>贝叶斯统计推断</h1><p>最大后验概率MAP</p><p>最小均方误差LMS</p><blockquote><p>1.最大后验概率是在观测条件x下,寻求待估量最可能的值作为估计值,即最大后验概率时的值。<br/>2.最小均方误差是在观测条件x下,以待估量的均值作为估计值。<br/></p></blockquote></article>";
JEditorPane editPane = new JEditorPane("text/html", html);
editPane.setEditable(false);
Dimension prefSize = editPane.getPreferredSize();
BufferedImage img = new BufferedImage(prefSize.width,prefSize.height, BufferedImage.TYPE_INT_ARGB);
Graphics graphics = img.getGraphics();
editPane.setSize(prefSize);
editPane.paint(graphics);
graphics.dispose();
ImageIO.write(img, "png", new File("/Users/yourname/Documents/work/temp/20190219.png"));
}
这是最简单的HTML代码了,但显示效果却不太好,如下图所示:
而且多尝试后发现JeditorPane解析稍微复杂一点的js页面就会出错。
html2image
这是百度来的,从maven上可以搜索到,下载之后发现其内部就是调用JEditorPane,只是作了简单的封装,同样不满意。
wkhtmltox
这是在StackOverFlow上搜索Html2Image时看到的一种方案。尝试以后,效果强于JEditorPane,考虑试用。如谷歌首页转化如下:
最终选取方案
wkhtmltox在三个方面均强于JEditorPane。wkhtmltox没有java版本,采用java代码调用linux命令的方式使用该工具包。
wkhtmltox分为wkhtmltoimage 和wkhtmltopdf,经过尝试wkhtmltoimage显示效果不太好,且可调参数较少。而wkhtmltopdf转化效果相当好,因此最终解决方案:
其中生成pdf使用wkhtmltopdf,pdf转化image使用pdfbox(apache出品,放心使用,效果也相当好).wkhtmltopdf是将整个浏览器显示效果转为为pdf,所以对于一些没有铺满整个浏览器的网页,此时的pdf含有一部分无用的空白,应当去掉。
先看最终效果:
代码
安装wkhtmltox
在https://wkhtmltopdf.org/下载安装。linux系统可使用wget下载。
安装成功测试命令:
wkhtmltopdf http://google.com google.pdf
帮助文档:
wkhtmltopdf -H
wkhtmltoimage -H
代码
命令:
wkhtmltopdf --javascript-delay 1000 -B 0mm -L 0mm -R 0mm -T 0mm url qptest.pdf
说明:javascript-delay是指最多等待js加载1000ms;B\L\R\T参数均是与生成的pdf文件的边距有关,可自行尝试。
对应上面流程图的方法调用:
html->htmlToPdf()->pdfToImage()->cutOffInvliadPart()->image
代码实现:
package test.java.com.action;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import javax.imageio.ImageIO;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException;
import org.apache.pdfbox.rendering.PDFRenderer;
public class HtmlToImage
{
public static void main(String[] args) throws Exception
{
Param0 param=new Param0();
param.setDpi("2");
param.setJavascriptDelay(1000);
param.setUrl("https://www.google.com/");
File pdf=htmlToPdf(param);
File image=pdfToImage(pdf, param);
}
/*
* 去掉无用的部分(pdf转成的图片有很大一部分是无用的白色背景)
* 找出图片的有效部分,把有效部分复制到另一张新图上。
*/
private static BufferedImage cutOffInvliadPart(BufferedImage image)
{
int width=image.getWidth();
int height=image.getHeight();
//记录原始图片每个像素的color
int[][] rgbs=new int[width][height];
//以下是图片有效部分的边界
int top=height;
int bottom=0;
int left=-1;
int right=-1;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
int rgb=image.getRGB(i, j);
rgbs[i][j]=rgb;
int tempRgb=rgb & 0x00ffffff;
boolean isWhite=(tempRgb==0x00ffffff);
if(!isWhite)
{
if(j<top)
{
top = j ;
}
if(j>bottom)
{
bottom=j;
}
if(left==-1)
{
left=i;
}
if(i>right)
{
right=i;
}
}
}
}
//留白
bottom=(bottom+10)>(height-1)?(height-1):(bottom+10);
left=(left>1)?(left-1):0;
System.out.println(String.format("left:%s right:%s top:%s bottom:%s", left,right,top,bottom));
//像素的坐标是从0算起的,所以宽高要+1.
BufferedImage newImage = new BufferedImage(right-left+1, bottom-top+1, BufferedImage.TYPE_INT_RGB);
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
if((j>=top) && (j<=bottom) && (i>=left) && (i<=right))
{
newImage.setRGB(i-left, j-top, rgbs[i][j]);
}
}
}
return newImage;
}
private static File pdfToImage(File pdf,Param0 param) throws InvalidPasswordException, IOException
{
String filePath=System.getProperty("java.io.tmpdir") + "/" + "test" + ".png";
System.out.println(filePath);
File file=new File(filePath);
try
{
PDDocument pdDoc=PDDocument.load(pdf);
PDFRenderer render=new PDFRenderer(pdDoc);
//1=72 dpi
BufferedImage image=render.renderImage(0,Float.parseFloat(param.getDpi()));
BufferedImage newImage = cutOffInvliadPart(image);
ImageIO.write(newImage, "png", new FileOutputStream(file));
return file;
}
finally
{
//如果上传到图片服务器上则删除本地文件
// if(file!=null && file.exists())
// {
// file.delete();
// }
if(pdf!=null && pdf.exists())
{
pdf.delete();
}
}
}
private static File htmlToPdf(Param0 param) throws IOException, InterruptedException
{
StringBuilder commandBuilder = new StringBuilder();
if (System.getProperty("os.name").contains("Mac") || System.getProperty("os.name").contains("mac"))
{
//我的本地配置
commandBuilder.append("/usr/local/bin/wkhtmltopdf");
}
else
{
//服务器配置
commandBuilder.append("/usr/local/bin/wkhtmltopdf");
}
//wkhtmltopdf --javascript-delay 1000 -B 1mm -L 1mm -R 1mm -T 1mm url qptest.pdf
commandBuilder.append(" --javascript-delay ").append(param.getJavascriptDelay() + " ")
.append(" -B 0mm -L 0mm -R 0mm -T 0mm ");
commandBuilder.append(param.getUrl());
String pdfPath = System.getProperty("java.io.tmpdir") + "/" + "test" + ".pdf";
File pdf = new File(pdfPath);
commandBuilder.append(" " + pdfPath);
String command = commandBuilder.toString();
System.out.println(command);
Process process = Runtime.getRuntime().exec(command);
process.waitFor();
return pdf;
}
/**
* 外部传入的参数
*/
private static class Param0
{
private String url;
private String dpi="2";
private int javascriptDelay=200;
public String getUrl()
{
return url;
}
public void setUrl(String url)
{
this.url = url;
}
public String getDpi()
{
return dpi;
}
public void setDpi(String dpi)
{
this.dpi = dpi;
}
public int getJavascriptDelay()
{
return javascriptDelay;
}
public void setJavascriptDelay(int javascriptDelay)
{
this.javascriptDelay = javascriptDelay;
}
}
}
不足之处
解决方案有以下不足:
- 需要在服务器上安装wkhtmltox
- 接口运行速度需要提高