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代码了,但显示效果却不太好,如下图所示:
Java图片处理:网页转图片(HtmlToImage)
而且多尝试后发现JeditorPane解析稍微复杂一点的js页面就会出错。

html2image

这是百度来的,从maven上可以搜索到,下载之后发现其内部就是调用JEditorPane,只是作了简单的封装,同样不满意。

wkhtmltox

这是在StackOverFlow上搜索Html2Image时看到的一种方案。尝试以后,效果强于JEditorPane,考虑试用。如谷歌首页转化如下:

最终选取方案

wkhtmltox在三个方面均强于JEditorPane。wkhtmltox没有java版本,采用java代码调用linux命令的方式使用该工具包。
wkhtmltox分为wkhtmltoimage 和wkhtmltopdf,经过尝试wkhtmltoimage显示效果不太好,且可调参数较少。而wkhtmltopdf转化效果相当好,因此最终解决方案:

Created with Raphaël 2.2.0html字符pdfimage优化image

其中生成pdf使用wkhtmltopdf,pdf转化image使用pdfbox(apache出品,放心使用,效果也相当好).wkhtmltopdf是将整个浏览器显示效果转为为pdf,所以对于一些没有铺满整个浏览器的网页,此时的pdf含有一部分无用的空白,应当去掉。
先看最终效果:
Java图片处理:网页转图片(HtmlToImage)

代码

安装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
  • 接口运行速度需要提高