基於PhantomJs的Java後臺網頁截圖技術

基於PhantomJs的Java後臺網頁截圖技術

公司以前作的某一手機應用,裏面有一需求是一鍵出圖(有一統計信息類的網頁,須要在不打開網頁的狀況下實時對網頁進行截圖而後保存到服務器上),手機上即可以查看該圖片了。剛開始拿到需求發現比較棘手,參考了不少文章解決方案大楷有如下幾種:javascript

  • Robot
  • 利用JNI,調用第三方C/C++組件
  • DJNativeSwing組件

參考文章:blog.csdn.net/cping1982/a…css

通過試驗Robot失敗,DJNativeSwing組件截圖成功,但因爲網頁css的複雜性致使圖片失真嚴重而達不到預期效果。而後繼續尋找解決方案,PlantomJs是最完美的解決方案。html

PlantomJs是一個基於javascript的webkit內核無頭瀏覽器 也就是沒有顯示界面的瀏覽器,你能夠在基於 webkit 瀏覽器作的事情,它都能作到。PlantomJs提供瞭如 CSS 選擇器、DOM操做、JSON、HTML五、Canvas、SVG 等。PhantomJS 的用處很普遍,如網絡監控、網頁截屏、頁面訪問自動化、無需瀏覽器的 Web 測試等,而博主只須要一很小的功能就是網頁截屏。java


實現思路

手機發送請求到服務器,服務器截取網頁爲圖片保存到硬盤,生成可訪問的URL返回手機上,示意圖以下:git


下載

直接進入官網下載http://phantomjs.org/download.html,目前官方支持三種操做系統,包括windows\Mac OS\Linux, 而博主服務器基於windows,因此下載https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-windows.zip,解壓後獲得如下目錄:github


編寫截圖JavaScript

參考文章http://www.cnblogs.com/jasondan/p/4108263.htmlweb

負責截圖腳本screenshot.js以下:spring

/**
 * phantomJs 腳本
 */
var page = require('webpage').create(), system = require('system'), address, output, size;

if (system.args.length < 3 || system.args.length > 5) {
	phantom.exit(1);
} else {
	address = system.args[1];
	output = system.args[2];
	//定義寬高
	page.viewportSize = {
		width : 800,
		height : 600
	};
	page.open(address, function(status) {
		var bb = page.evaluate(function() {
			return document.getElementsByTagName('html')[0].getBoundingClientRect();
		});
		page.clipRect = {
			top : bb.top,
			left : bb.left,
			width : bb.width,
			height : bb.height
		};
		window.setTimeout(function() {
			page.render(output);
			page.close();
			console.log('渲染成功...');
		}, 1000);
	});
}
複製代碼
address = system.args[1];//傳入的URL地址
output = system.args[2];//保存的圖片路徑
複製代碼

以上是screenshot.js 的腳本內容json


編寫服務器Java代碼

public static void main(String[] args) throws IOException {
		String BLANK = " ";
		Process process = Runtime.getRuntime().exec(
				"D:/develop_software/phantomjs/bin/phantomjs.exe" + BLANK //你的phantomjs.exe路徑
				+ "D:/screenshot.js" + BLANK //就是上文中那段javascript腳本的存放路徑
				+ "http://www.baidu.com" + BLANK //你的目標url地址
				+ "D:/baidu.png");//你的圖片輸出路徑

		InputStream inputStream = process.getInputStream();
		BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
		String tmp = "";
		while ((tmp = reader.readLine()) != null) {
			if (reader != null) {
				reader.close();
			}
			if (process != null) {
				process.destroy();
				process = null;
			}
			System.out.println("渲染成功...");
		}
	}
複製代碼

運行以上java代碼,會在d盤下生成baidu.png的圖片截圖成功以下圖:windows

至此一個demo完成!


代碼封裝(實際項目)

一、screenshot.js處理

實際應用中相似於screenshot.js 通常不放在固定目錄,通常放在應用根目錄下

在tomcat啓動時就把screenshot.js 路徑緩存起來

/** * 獲取【網頁快照截圖腳本】文件的路徑 * * @return */
private String getFullJsPath() {
	return AppContext.getAbsPath() + "/apicture/js/screenshot.js";
}
複製代碼

二、phantomjs.exe處理

把phantomjs.exe的路徑配置化,不直接像demo中那樣寫死到程序中,在web應用中通常都有一個總的applicationConfig.xml來存放諸如這種東西,因而在applicationConfig.xml中加入以下xml節點:

...
<phantomJs>
	<bin>D:/develop_software/phantomjs/bin/phantomjs.exe</bin>
	<imagePath>apicture/pub</imagePath><!--圖片生成路徑-->
</phantomJs>
...
複製代碼

經過jaxb工具包將配置轉化到對象中

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

/** * phantomJs 配置 * * @author Fhcj * 2016年8月26日 * @since * @version */
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "phantomJs")
public class PhantomJsConifg {

	@XmlElement(name = "bin")
	private String bin;
	@XmlElement(name = "imagePath")
	private String imagePath;

	public String getBin() {
		return bin;
	}

	public void setBin(String bin) {
		this.bin = bin;
	}

	public String getImagePath() {
		return imagePath;
	}

	public void setImagePath(String imagePath) {
		this.imagePath = imagePath;
	}

}
複製代碼

三、編寫action

博主用的是nutz mvc做爲action層,同理可用servlet或者spring mvc、struts2等

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.rbp.rt.web.result.ObjectResult;
import com.*.eqma.AppContext;
import com.*.eqma.config.PhantomJsConifg;
import com.*.eqma.web.servlet.YingJiJspServlet;
import com.*.eqma.web.servlet.ZaiQingJspServlet;
import com.*.eqma.web.servlet.ZhenQingJspServlet;
import com.*.utils.DateUtils;
import com.*.utils.JsonUtils;
import com.*.utils.StringUtils;

/** * * 一張圖訪問服務 * * @author Fhcj * 2016年9月2日 * @since * @version */
@Ok("json")
public class APictureAction {

	private static final Logger LOG = LoggerFactory.getLogger(APictureAction.class);
	private static final String BLANK = " ";
	private static PhantomJsConifg pjc;

	private String BIN_PATH;
	private String IMAGE_PUB_PATH;

	/** * 應急一張圖 * * @param evt_id * 事件id * @return */
	@SuppressWarnings("unused")
	@At("/apicture/yingJi")
	public String yingJiPicture(@Param("evt_id") String evt_id) {
		ObjectResult responseResult = new ObjectResult();

		if (StringUtils.isEmpty(evt_id)) {
			responseResult.setNote("地震事件Id爲空,沒法渲染圖片");
			responseResult.setStatus(-1);
			return JsonUtils.obj2Json(responseResult);
		}

		
		String pictureName = evt_id + "_yingJi.png";
		try {
			String imgageFullPath = getFullImagePath(pictureName);// 獲得圖片完整路徑
			// 若是該事件的一張圖存在則不用渲染
			if (new File(imgageFullPath).exists()) {
				LOG.info("事件ID爲【{}】的【應急一張圖】已經存在,將不會從新渲染:{}", evt_id, imgageFullPath);
				responseResult.setValue(getPictureVisitURL(pictureName));
				responseResult.setStatus(1);
				return JsonUtils.obj2Json(responseResult);
			}

			String url = YingJiJspServlet.getURL() + "?id=" + evt_id;// 應急一張圖訪問接口URL

			Process process = Runtime.getRuntime().exec(cmd(imgageFullPath, url));

			InputStream inputStream = process.getInputStream();
			BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
			String tmp = "";
			while ((tmp = reader.readLine()) != null) {
				close(process, reader);

				LOG.info("事件ID爲【{}】的【應急一張圖】渲染成功:{}", evt_id, imgageFullPath);
				LOG.info("事件ID爲【{}】的【應急一張圖】訪問路徑爲:{}", evt_id, getPictureVisitURL(pictureName));
				responseResult.setValue(getPictureVisitURL(pictureName));
				responseResult.setStatus(1);
				return JsonUtils.obj2Json(responseResult);
			}
		} catch (Exception e) {
			responseResult.setStatus(-1);
			responseResult.setNote("事件ID爲【{}】的【應急一張圖】渲染失敗");
			LOG.error("事件ID爲【{}】的【應急一張圖】渲染失敗:", e);
		}

		return JsonUtils.obj2Json(responseResult);
	}

	/** * 災情一張圖 * * @param evt_id * 事件id * @return */
	@SuppressWarnings("unused")
	@At("/apicture/zaiQing")
	public String zaiQingPicture(@Param("evt_id") String evt_id) {
		ObjectResult responseResult = new ObjectResult();

		if (StringUtils.isEmpty(evt_id)) {
			responseResult.setNote("地震事件Id爲空,沒法渲染圖片");
			responseResult.setStatus(-1);
			return JsonUtils.obj2Json(responseResult);
		}

		String pictureName = evt_id + "_zaiQing.png";
		try {
			String imgageFullPath = getFullImagePath(pictureName);

			// 若是該事件的一張圖存在則不用渲染
			if (new File(imgageFullPath).exists()) {
				LOG.info("事件ID爲【{}】的【災情一張圖】已經存在,將不會從新渲染:{}", evt_id, imgageFullPath);
				responseResult.setValue(getPictureVisitURL(pictureName));
				responseResult.setStatus(1);
				return JsonUtils.obj2Json(responseResult);
			}

			String url = ZaiQingJspServlet.getURL() + "?id=" + evt_id;// 災情一張圖訪問接口URL
			Process process = Runtime.getRuntime().exec(cmd(imgageFullPath, url));

			InputStream inputStream = process.getInputStream();
			BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
			String tmp = "";
			while ((tmp = reader.readLine()) != null) {
				close(process, reader);

				LOG.info("事件ID爲【{}】的【災情一張圖】渲染成功:{}", evt_id, imgageFullPath);
				LOG.info("事件ID爲【{}】的【災情一張圖】訪問路徑爲:{}", evt_id, getPictureVisitURL(pictureName));
				responseResult.setValue(getPictureVisitURL(pictureName));
				responseResult.setStatus(1);
				return JsonUtils.obj2Json(responseResult);
			}
		} catch (Exception e) {
			responseResult.setStatus(-1);
			responseResult.setNote("事件ID爲【{}】的【災情一張圖】渲染失敗");
			LOG.error("事件ID爲【{}】的【災情一張圖】渲染失敗:", e);
		}

		return JsonUtils.obj2Json(responseResult);
	}
	/** * 震情一張圖 * * @param lng * 經度 * @param lat * 緯度 * @return */
	@SuppressWarnings("unused")
	@At("/apicture/zhenQing")
	public String zhenQingPicture(@Param("lng") String lng, @Param("lat") String lat) {
		ObjectResult responseResult = new ObjectResult();
		String pictureName = DateUtils.formatCurrentDate("yyyyMMddHHmmssSSS") + "_zhenQing.png";
		try {

			String imgageFullPath = getFullImagePath(pictureName);

			String url = ZhenQingJspServlet.getURL() + "?lng=" + lng + "&lat=" + lat;// 震情一張圖訪問接口URL

			Process process = Runtime.getRuntime().exec(cmd(imgageFullPath, url));

			InputStream inputStream = process.getInputStream();
			BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
			String tmp = "";
			while ((tmp = reader.readLine()) != null) {
				close(process, reader);

				LOG.info("【震情一張圖】渲染成功:{}", imgageFullPath);
				LOG.info("【震情一張圖】訪問路徑爲:{}", getPictureVisitURL(pictureName));
				responseResult.setValue(getPictureVisitURL(pictureName));
				responseResult.setStatus(1);
				return JsonUtils.obj2Json(responseResult);
			}
		} catch (Exception e) {
			responseResult.setStatus(-1);
			responseResult.setNote("【震情一張圖】渲染失敗");
			LOG.error("【震情一張圖】渲染失敗:", e);
		}

		return JsonUtils.obj2Json(responseResult);
	}

	/** * 獲取執行JS腳本的window cmd 命令 * * @param imgageFullPath * 圖片完整路徑 * @param url * 截圖網頁的URL * @return */
	private String cmd(String imgageFullPath, String url) {
		return getBinPath() + BLANK + getFullJsPath() + BLANK + url + BLANK + imgageFullPath;
	}

	/** * 關閉進程 * * @param process * @param bufferedReader * @throws IOException */
	private void close(Process process, BufferedReader bufferedReader) throws IOException {
		if (bufferedReader != null) {
			bufferedReader.close();
		}
		if (process != null) {
			process.destroy();
			process = null;
		}
	}

	/** * 經過圖片名獲取最終【客戶端】訪問的URL * * @param pictureName * @return */
	private String getPictureVisitURL(String pictureName) {
		return AppContext.getDomain() + "/" + pjc.getImagePath() + "/" + pictureName;
	}

	/** * 經過圖片名獲取最終完整路徑 * * @param pictureName * @return */
	private String getFullImagePath(String pictureName) {
		return getPictureRootPath() + "/" + pictureName;
	}

	/** * 獲取【網頁快照截圖腳本】文件的路徑 * * @return */
	private String getFullJsPath() {
		return AppContext.getAbsPath() + "/apicture/js/screenshot.js";
	}

	/** * 獲取圖片生成的根路徑 * * @return */
	private String getPictureRootPath() {
		ensurePhantomJsConfig();
		IMAGE_PUB_PATH = AppContext.getAbsPath() + "/" + pjc.getImagePath();
		return IMAGE_PUB_PATH;
	}

	/** * 獲取phantomjs.exe所在路徑 * * @return */
	private String getBinPath() {
		ensurePhantomJsConfig();
		BIN_PATH = pjc.getBin();
		return BIN_PATH;
	}

	/** * 確保配置存在 */
	private void ensurePhantomJsConfig() {
		if (pjc == null) {
			pjc = AppContext.getApplicationConfig().getPhantomJsConifg();
		}
	}
}
複製代碼

因而訪問http://localhost:8080/xxx/apicture/zhenQing便會返回圖片的URL,手機端即可查看展現以下:

相關文章
相關標籤/搜索