使用phantomJS將html轉為圖片
阿新 • • 發佈:2018-12-16
- 問題描述
近期寫稿專案碰到一個問題,由於文章會發布到不同裝置和平臺上,在前端展示的時候可能會與平臺本身的樣式發生覆蓋,導致表格樣式顯示不正常。短時間內想要做出一個適應所有環境的前端樣式不太現實。因為使用本地模板生成的表格不存在樣式問題,所以考慮將本地html模板中的<table></table>標籤內容轉換為圖片並在原位置替換。
- 解決思路
使用phantomJs模擬瀏覽器訪問html模板,用選擇器擷取dom節點,獲取<table>標籤並把表格放入新建的canvas畫布
- 實施步驟
1.安裝phantomjs並配置環境變數
2.使用cmd命令執行phantomjs指令碼,使用phantom訪問瀏覽器的指令碼
import org.dom4j.DocumentException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; /** * html dom轉圖片 * * @author Hongyi Zheng * @date 2018/7/31 */ @Component("dom2ImageService") public class Dom2ImageService { @Value("${server.port}") private String port; @Value("${server.context-path}") private String contextPath; private static final Logger logger = LoggerFactory.getLogger(Dom2ImageService.class); private final ArticleBean articleBean; @Autowired FileUploadService fileUploadService; @Autowired PageContentService pageContentService; @Autowired ArticleTraceService articleTraceService; @Autowired public Dom2ImageService(ArticleBean articleBean) { this.articleBean = articleBean; } public void convert2Img(String traceId,String tblName,String tblContent) throws IOException { logger.info("[" + traceId + "]表格{}轉換圖片中...", tblName); //拼接phantom命令列/引數 StringBuilder phantomCmd = new StringBuilder(); String osName = System.getProperties().getProperty("os.name").toLowerCase(); String tmpPath = articleBean.getD2ImgTemplatePath(); String htmlName = traceId + tblName + ".html"; String imgName = traceId + tblName + ".png"; List<ArticleTrace> list = articleTraceService.selectByTraceId(traceId); String tmpName = ""; if (null != list && list.size() > 0) { tmpName = list.get(0).getTempName(); } String date = DateUtils.format(new Date(), DateUtils.STYLE_yyyyMMdd); String lxPath = String.format("/opt/app/applications/xxxxx/temp/%s/%s/%s/%s", date, tmpName, "tmp", htmlName); String winPath = String.format("%s%s\\%s\\%s\\%s", articleBean.getArticleLocalPath(), date, tmpName, "tmp", htmlName); String target; String dest; if (osName.contains("linux")) { target = lxPath; dest = String.format("opt/app/applications/xxxxx/temp/%s/%s/%s", date, tmpName, imgName); phantomCmd.append(articleBean.getPhantomjsPath()) .append(" ") .append(articleBean.getD2ImgJsPath()) .append(" http://localhost:").append(port).append(contextPath).append(tmpPath) .append(" ") .append(" \"") .append(TableUtils.strTrans(tblContent, true)) .append("\" ") .append(target); }else { target = winPath; dest = String.format("%s%s\\%s\\%s\\%s", articleBean.getArticleLocalPath(), date, tmpName, "tmp", imgName); phantomCmd.append(articleBean.getPhantomjsPath()) .append(" ") .append(articleBean.getD2ImgJsPath()) .append(" http://localhost:").append(port).append(contextPath).append(tmpPath) .append(" \"") .append(TableUtils.strTrans(tblContent, false)) .append("\" ") .append(target); } Process process = TableUtils.executeTbl(phantomCmd.toString()); String info = ExecuteUtils.getInputInfo(process); logger.info("phantomjs log = {}",info); if (null != process) { try { process.waitFor(); } catch (InterruptedException e) { e.printStackTrace(); } } //wkhtml將html轉成image StringBuilder wkCmd = new StringBuilder(); if (osName.contains("linux")) { wkCmd.append("wkhtmltoimage --encoding utf8 ").append(target).append(" ").append(dest); }else { wkCmd.append("wkhtmltoimage ").append(target).append(" ").append(dest); } Process p = TableUtils.executeTbl(wkCmd.toString()); if (null != p) { try { p.waitFor(); } catch (InterruptedException e) { e.printStackTrace(); } } logger.info("[" + traceId + "]" + "{}渲染完畢", tblName); PageContent tbl = new pageContent(); tbl.setTraceId(traceId); tbl.setContentKey(tblName); String img = ImageUtils.toBase64Str(dest); //替換base64頭 if (img.contains(Constants.BASE64_HEADER_JPG)) { img = img.substring(img.indexOf(Constants.BASE64_HEADER_JPG) + Constants.BASE64_HEADER_JPG.length()); } else if (img.contains(Constants.BASE64_HEADER_PNG)) { img = img.substring(img.indexOf(Constants.BASE64_HEADER_PNG) + Constants.BASE64_HEADER_PNG.length()); } //壓縮圖片 ImageUtils.compress(dest); //img 標籤圖片src String contentValue = "<img src = \"" + src + "\" alt = \"\"/>"; List<PageContent> pageContents = pageContentService.selectByTraceAndKey(tbl); if (null != pageContents && pageContents.size() > 0) { tbl = pageContents.get(0); tbl.setIsDel(Constants.IS_DEL_NORMAL); tbl.setOutime(new Date()); tbl.setContentValue(contentValue); //落庫 pageContentService.updateSelective(tbl); } else { tbl.setContentValue(contentValue); tbl.setContentType(Constants.TYPE_TBL); pageContentService.insert(tbl); } } } import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sun.misc.BASE64Decoder; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.*; /** * @author Hongyi Zheng * @date 2018/7/30 */ public class TableUtils { private static final Logger logger = LoggerFactory.getLogger(TableUtils.class); public static Process executeTbl(String cmd){ logger.info("命令列執行:"+cmd); Runtime rt = Runtime.getRuntime(); try { return rt.exec(cmd); } catch (IOException e) { logger.error("phantomjs IO異常,cmd = {}", cmd); return null; } } /** * 命令列字串轉義 * @param str 初始字串 * @return */ public static String strTrans(String str,boolean isLinux){ if (isLinux) { return str.replaceAll("<","\\<"); }else { return str.replace("\"", "\\\""); } } } import net.coobird.thumbnailator.Thumbnails; import sun.misc.BASE64Encoder; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.text.DecimalFormat; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; public class ImageUtils { //壓縮圖片 public static void compress(String path){ try { Thumbnails.of(path).scale(1).outputQuality(1).toFile(path); } catch (IOException e) { e.printStackTrace(); } } public static String toBase64Str(String imgPath){ InputStream in = null; byte[] data = null; // 讀取圖片位元組陣列 try { in = new FileInputStream(imgPath); data = new byte[in.available()]; in.read(data); in.close(); } catch (IOException e) { e.printStackTrace(); } // 對位元組陣列Base64編碼 BASE64Encoder encoder = new BASE64Encoder(); // 返回Base64編碼過的位元組陣列字串 return encoder.encode(data); } }
phantomJS指令碼如下:
//phantomJS指令碼 var page = require('webpage').create(), system = require('system'), url, tblContent, dest; if (system.args.length === 0) { console.log('phantom : 引數錯誤!'); phantom.exit(); } else if (system.args.length === 1) { console.log('phantom : url未指定!'); phantom.exit(); } else { start = Date.now(); url = system.args[1]; tblContent = system.args[2]; dest = system.args[3]; var fs = require("fs"); //輸出訪問webpage頁面的log page.onConsoleMessage = function (msg) { console.log(msg); }; //viewportSize being the actual size of the headless browser page.viewportSize = {width: 1024, height: 768}; page.open(url, function (status) { if (status === 'success') { //頁面開啟成功則呼叫appendDiv()函式 var tbl = page.evaluate(function(tblContent){ appendDiv(tblContent); return document.getElementById('table').getBoundingClientRect(); },tblContent); console.log('phantom : 頁面載入成功,用時:' + (Date.now() - start) + 'ms'); try { fs.write(dest, page.content, 'w'); console.log('phantom : 本地檔案寫入成功' + dest); } catch (e) { console.error(e); } } else { console.log('phantom : 頁面載入失敗,status:' + status); } //exit phantomJs in 1 secs setTimeout(function () { phantom.exit(); }, 1000); }); }
3.phantomjs訪問html模板
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script type="text/javascript" src="../../js/jquery-1.12.0.min.js"></script>
<script type="text/javascript" src="../../js/promise-6.1.0.js"></script>
<style>
* {
margin: 0;
padding: 0
}
#table {
width: 50%;
margin: 0 auto;
}
table {
border-collapse: collapse;
border-spacing: 0;
text-align: center;
width: 100%;
font-size: 14px;
}
table tr th {
background-color: #ffc599;
}
table tr th, table tr td {
border: 1px solid #ddd;
padding: 5px;
text-align: center;
-webkit-text-size-adjust: none;
word-break: break-all;
}
/*table tr td:first-child {
font-weight: 500;
background-color: #eef5fc;
}*/
</style>
</head>
<body>
<div id="table"></div>
<script type="text/javascript">
function appendDiv(tblContent) {
var ele = document.getElementById('table');
ele.innerHTML = tblContent;
console.log('div填充完畢');
}
</script>
</body>
</html>
4.主要使用的工具:
基於QtWebKit核心的無頭瀏覽器,可以完成模擬瀏覽器行為,後臺操作頁面。常用於dom操作,CSS選擇器,web測試,爬蟲
可選的圖片渲染工具wkhtml2image/html2canvas
5.在html頁面使用html2canvas對錶格渲染成圖片後,需要使用ajax回撥後端介面,注意可能導致跨域問題
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script type="text/javascript" src="../../js/jquery-1.12.0.min.js"></script>
<script type="text/javascript" src="../../js/html2canvas.js"></script>
<script type="text/javascript" src="../../js/canvas2image.js"></script>
<script type="text/javascript" src="../../js/promise-6.1.0.js"></script>
<style>
* {
margin: 0;
padding: 0
}
#table {
width: 780px;
margin: 0 auto;
}
table {
border-collapse: collapse;
border-spacing: 0;
text-align: center;
width: 100%;
font-size: 14px;
}
table tr th {
background-color: #ffc599;
}
table tr th, table tr td {
border: 1px solid #ddd;
padding: 5px;
text-align: center;
-webkit-text-size-adjust: none;
word-break: break-all;
}
table tr td:first-child {
font-weight: 500;
background-color: #eef5fc;
}
</style>
</head>
<body>
<div id="table"></div>
<script type="text/javascript">
function convert2img(tblContent, traceId, tblName) {
var cntElem = document.getElementById('table');
cntElem.innerHTML = tblContent;
setTimeout(function () {
//需要截圖的包裹的(原生的)DOM 物件
var shareContent = cntElem;
//獲取dom 寬度
var width = shareContent.offsetWidth;
//獲取dom 高度
var height = shareContent.offsetHeight;
//建立一個canvas節點
var canvas = document.createElement("canvas");
//定義任意放大倍數 支援小數
var scale = 8;
//定義canvas 寬度 * 縮放
canvas.style.width = width + 'px';
canvas.width = width * scale;
//定義canvas高度 *縮放
canvas.style.height = height + 'px';
canvas.height = height * scale;
//獲取context,設定scale
canvas.getContext("2d").scale(scale, scale);
var opts = {
// 新增的scale 引數
scale: scale,
//自定義 canvas
canvas: canvas,
//日誌開關,便於檢視html2canvas的內部執行流程
// logging: true,
//dom 原始寬度
width: width,
height: height,
// 使用cross-Domain開啟跨域配置
useCORS: true
};
html2canvas(shareContent, opts).then(function (canvas) {
var context = canvas.getContext('2d');
//禁用圖片平滑處理
context.mozImageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false;
context.msImageSmoothingEnabled = false;
context.imageSmoothingEnabled = false;
//將圖片轉為JPEG格式並設定canvas畫布的寬高(可選BMP/PNG等格式)
var img = Canvas2Image.convertToJPEG(canvas, canvas.width, canvas.height);
cntElem.appendChild(img);
/*$(img).css({
"width": canvas.width / 4 + "px",
"height": canvas.height / 4 + "px"
}).addClass('f-full');*/
var base64img = $('img').attr('src');
$.ajax({
async: false,
type: 'POST',
url: '/xxxxx/canvas/tbl2img',
data: {
img: base64img, traceId: traceId, tblId: tblName
},
success: function () {
console.log("回撥成功!")
},
error: function () {
console.log('回撥失敗!');
}
});
});
}, 1000);
}
</script>
</body>
</html>
好了,這樣就完成了把html表格標籤轉為圖片儲存。