- 问题描述
近期写稿项目碰到一个问题,由于文章会发布到不同设备和平台上,在前端展示的时候可能会与平台本身的样式发生覆盖,导致表格样式显示不正常。短时间内想要做出一个适应所有环境的前端样式不太现实。因为使用本地模板生成的表格不存在样式问题,所以考虑将本地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.主要使用的工具:
phantomjs:http://phantomjs.org/
基于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表格标签转为图片保存。