1. 程式人生 > >使用phantomJS將html轉為圖片

使用phantomJS將html轉為圖片

  • 問題描述

近期寫稿專案碰到一個問題,由於文章會發布到不同裝置和平臺上,在前端展示的時候可能會與平臺本身的樣式發生覆蓋,導致表格樣式顯示不正常。短時間內想要做出一個適應所有環境的前端樣式不太現實。因為使用本地模板生成的表格不存在樣式問題,所以考慮將本地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表格標籤轉為圖片儲存。