web實時長圖實踐
背景簡介
全民K歌專輯釋出新玩法,傳統宣傳專輯戰績的流程,從獲取資料,到製作海報,到傳播,週期長運營成本高,如何快速分享戰績進行榮譽感的傳播成為一個亟待解決的問題。
產品:能不能在專輯大事件觸發時,自動生成一個大事件長圖,供粉絲分享傳播?
開發:理論上沒問題,嘗試下吧…
瀏覽器端實現方案
開發:大事件長圖和專輯詳情頁大事件tab的視覺效果基本一致,如果能複用可以減少開發時間。
開發:怎麼複用呢?
於是便有了下面在瀏覽器端嘗試dom轉圖片的兩種方案:
html2canvas
html2canvas一個在瀏覽器端通過JS對整個或部分頁面進行“截圖”的庫。
html2canvas使用方法簡單,截圖的核心程式碼如下:
let imgBase64;
html2canvas(htm,{
onrendered : function(canvas){
//生成base64圖片資料
imgBase64 = canvas.toDataURL();
});
使用簡單,但是坑不少,遇到的坑及解決方案:
1.截圖模糊
主要解決思路:
1)將canvas的width和height屬性放大為2倍。
2)將canvas的CSS樣式width和height設定為原先1倍的大小。
<canvas width="200" height="100" style="width:100px;height:50px;"></canvas>
2.截圖不全
原始碼獲取dom高度不準確,修改原始碼,獲取高度後手動傳,修改方式如下:
原始碼:
return renderDocument(node.ownerDocument, options, node.ownerDocument.defaultView.innerWidth, node.ownerDocument.defaultView.innerHeight, index).then(function(canvas) { if (typeof(options.onrendered) === "function") { log("options.onrendered is deprecated, html2canvas returns a Promise containing the canvas"); options.onrendered(canvas); } return canvas; });
修改後
//新增自定義高度寬度
var width = options.width != null ? options.width : node.ownerDocument.defaultView.innerWidth;
var height = options.height != null ? options.height : node.ownerDocument.defaultView.innerHeight;
return renderDocument(node.ownerDocument, options, width, height, index).then(function (canvas) {
if (typeof(options.onrendered) === "function") {
log("options.onrendered is deprecated, html2canvas returns a Promise containing the canvas");
options.onrendered(canvas);
}
return canvas;
});
3.截圖慢
截圖慢得從html2canvas的原理說起,html2canvas並不是真正的截圖,而是遍歷載入的頁面DOM,收集所有元素的資訊,然後基於從DOM讀取的屬性使用canvas來繪製。
基於這個截圖原理,慢的問題優化空間不大,而且html2canvas還有些CSS的限制,它只能正確地呈現它支援的CSS屬性,完整的CSS屬性支援列表,可以在官網檢視。
關於慢,最簡單的解決方案是在使用者操作前提前生成截圖。
4.crash
html2canvas截圖後,將圖片的base64傳到客戶端的分享元件,當base64超過500k可能導致客戶端卡死或crash,如果慢的問題還能忍,那這個問題是真的沒法接受的。
svg
除了html2canvas網上也有更輕量更快的庫,這些庫是基於svg的,嘗試了下確實比html2canvas快很多。
svg方案的嘗試:
//要轉成圖片的dom
let htm = '<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="auto"><foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml"><div>這裡是頁面內容...</div></div></foreignObject></svg>';
let DOMURL = window.URL || window.webkitURL || window;
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
let img = new Image();
let svg = new Blob([htm], {type: 'image/svg+xml;charset=utf-8'});
let url = DOMURL.createObjectURL(svg);
let imgBase64;
img.onload = function () {
ctx.drawImage(img, 0, 0);
imgBase64 = canvas.toDataURL();
}
img.src = url;
svg方案沒法繞過的坑:
1.ios下不支援跨域圖片
由於安全限制,ios下跨域圖片加crossOrigin
屬性也沒法繞過跨域問題。
2.crash
和html2canvas一樣,svg轉圖片後最終也是轉base64傳分享元件,base64超過500K可能導致的卡死和crash問題也存在。
伺服器端實現方案
開發:瀏覽器端的方案crash問題不能忍,不如在伺服器端生成圖片,傳圖片URL到分享元件?
本著最大限度複用程式碼的初衷,首選了無頭瀏覽器phantomjs截圖的方案。
PhantomJS
PhantomJS是基於WebKit核心的無頭瀏覽器,提供瀏覽器環境的命令列介面,我們可以進行網頁截圖、抓取網頁資料等操作,更多詳情可以去PhantomJS官網檢視。
安裝PhantomJS時,注意安裝以下依賴:
sudo yum -y install gcc gcc-c++ make flex bison gperf ruby openssl-devel freetype-devel fontconfig-devel libicu-devel sqlite-devel libpng-devel libjpeg-devel
伺服器端方案選擇的是phantomjs-node庫,實現截圖的核心程式碼如下:
var sitepage = null;
var phInstance = null;
phantom.create()
.then(instance => {
phInstance = instance;
return instance.createPage();
})
.then(page => {
let htm = [
'<!DOCTYPE html>',
'<html lang="zh-cn">',
'<head>',
'<meta charset="utf-8">',
'</head>',
'<body style="background:#fff">',
'<div>'+ new Date() +'</div>',
'</body>',
'</html>'
].join("");
page.property('content',htm);
page.render('./test.png').then((err) => {
phInstance.exit()
}).catch(err => {
phInstance.exit();
})
})
.catch(error => {
phInstance.exit();
});
PhantomJS遇到的坑也不少,主要是環境問題:
1.沒截圖生成
開發:在mac上和windows上生成截圖正常,部署到測試環境後不能生成截圖,列印PhantomJS日誌,沒有明確的報錯資訊。linux下許可權問題?
檢視PhantomJS和目錄許可權,PhantomJS沒有寫許可權,修復許可權問題,圖片仍然不能生成。
開發:字母命名的截圖正常生成,不支援圖片檔名包含數字?
一番驗證,截圖名包含數字phantomjs-node不能正常生成圖片檔案。
2.截圖空白
開發:顏色和圖案均能夠渲染到截圖中,只有文字不能渲染,字型有問題?
確認測試機中字型目錄為空,更新字型,文字終於能正常渲染到截圖中。
3.截圖模糊
又是模糊問題…
css使用相對rem單位,PhantomJS截圖是設定縮放參數:
//css
html{font-size: 100px;}
.owner_avatar{width:.30rem;height: .30rem;border-radius: .30rem;margin-right: .10rem;}
.events_img{width: .50rem;height:.50rem;}
//phantomjs縮放處理
page.property('viewportSize',{width:828,height:736});
page.property('zoomFactor',2)
page.property('content',htm);
4.截圖載入慢
模糊問題設定2倍圖後,圖片大小暴漲到6M+,導致載入慢,設定截圖質量:
page.render(fileName,{quality:85}).then((err) => {
phInstance.exit();
})
5.截圖慢
PhantomJS生成一個最簡單的截圖,耗時2S左右,這個速度顯然是不能接受的,暫時沒找到比較好的優化方式。
node canvas
node canvas擴充套件了canvas API以提供與節點的介面,例如流式傳輸PNG資料,轉換為Buffer例項等,更多介紹可以去node canvas官網檢視。
node canvas的環境搭建比較麻煩,依賴庫與PhantomJS類似,這裡就不列舉了。
繪製圖片的核心程式碼:
const { createCanvas, loadImage } = require('canvas');
const canvas = createCanvas(200, 200);
const ctx = canvas.getContext('2d');
ctx.font = '30px';
ctx.fillText('test', 50, 100);
loadImage('test.jpg').then((image) => {
ctx.drawImage(image, 0, 0, 70, 70);
})
node canvas與下面imagemagick的方案對比,imagemagick的效能更好,node canvas沒再深入只實現了簡單demo,踩坑不多。
ImageMagick 與 GraphicsMagick
ImageMagick是一套功能強大、穩定而且免費的工具集和開發包,可以用來讀、寫和處理超過90種的圖片檔案,包括流行的TIFF、JPEG、GIF、 PNG、PDF以及PhotoCD等格式。
ImageMagick可以根據web應用程式的需要動態生成圖片, 還可以對一個(或一組)圖片進行改變大小、旋轉、銳化、減色或增加特效等操作,並將操作的結果以相同格式或其它格式儲存,對圖片的操作,即可以通過命令列進行,也可以用C/C++、Perl、Java、PHP、Python或Ruby程式設計來完成。更多詳情可在ImageMagick官網檢視。
GraphicsMagick是從 ImageMagick 5.5.2 分支出來的,據說它變得更穩定和優秀,更多詳情可在GraphicsMagick官網檢視。
看起來GraphicsMagick是更好的選擇,但是由於node gm這個庫沒有實現GraphicsMagick的半透明和圓角支援,而且針對專輯的大事件長圖做了一些效能對比兩者差異不大,所以選擇使用ImageMagick。
node gm切換ImageMagick的方式非常簡單,只要加以下設定:
var gm = require('gm');
var imageMagick = gm.subClass({ imageMagick: true });
不可避免的,使用ImageMagick也遇到一些坑:
1.半透明遮罩
設計:專輯封面背景使用白透明遮罩,遮罩的顏色根據封面圖來定,深色封面圖用白色文字,淺色封面圖用黑色文字。
開發:OK,先canvas獲取封面圖顏色資訊,再判斷顏色深淺
//RGB與YUV互轉,Y>=128 為淺色
Y'= 0.299*R' + 0.587*G' + 0.114*B'
U'= -0.147*R' - 0.289*G' + 0.436*B' = 0.492*(B'- Y')
V'= 0.615*R' - 0.515*G' - 0.100*B' = 0.877*(R'- Y')
R' = Y' + 1.140*V'
G' = Y' - 0.394*U' - 0.581*V'
B' = Y' + 2.032*U'
//ImageMagick設定透明色
.fill("rgba(0,0,0,.5)")
2.頭像圓角
設計:這些頭像要用圓角哦。
開發:OK(還好ImageMagick支援圓角)
.fill("avatar.jpg")
.drawCircle(80,120,30,120)
ImageMagick圓角圖片實現方式與canvas類似,畫一個圓,然後用頭像圖片去填充來實現頭像圓角。
3.暱稱emoji表情
ImageMagick繪製暱稱中的表情圖比較麻煩,使用支援emoji的字型,嘗試過Twitter的彩色emoji字型,但是ImageMagick有BUG,不能還原為彩色的。
最終解決方案:
1)使用等寬字型,方便計算精確的emoji位置
2)ImageMagick繪製暱稱中的表情圖片
.draw("image Over " + size + " " + url)
ImageMagick效能優化:
優化前:
優化後:
ImageMagick生成單張圖片耗時100ms左右,但是併發請求多了平均耗時就暴漲到3S+,這個速度顯然是不能接受的,經過一番優化後將平均耗時降到1S左右,主要優化點如下:
1.gm程式碼拼接,VM中執行
多次呼叫gm多次操作圖片,嚴重影響效能,將圖片操作程式碼拼接成字串,在VM中執行,只調用一次gm,核心程式碼如下:
let sandbox = {
gm : imageMagick,
start : Date.now()
}
//計算圖片高度
let offset = getOffset();
let qrcodeStr = getQrcodeStr();
let titleStr = (function(){
return [
'.fontSize(24)',
'.fill("gray")',
'.drawText(164,152,"我是標題")'
];
})();
let str = 'gm(828,'+ offset.height +',"#fff").font("'+ FONTS +'",48)'+ titleStr + qrcodeStr +'.quality(90).write("test.jpg",function(err){console.log(err || Date.now() - start)})';
let script = new vm.Script(str);
let context = vm.createContext(sandbox);
script.runInContext(context);
2.mpc格式
mpc是ImageMagick提供的一種持久快取記憶體格式,減少對影象格式進行解碼和編碼畫素的開銷。
mpc生成兩個檔案:
1)一個副檔名.mpc保留了與影象或影象序列相關的所有屬性(例如寬度,高度,色彩空間等)。
2)一個副檔名.cache,是本地原始格式的畫素快取。
讀取mpc影象檔案時,ImageMagick讀取影象屬性,並將記憶體對映到磁碟上的畫素快取,無需解碼影象畫素,不過mpc的檔案大小比其他影象格式大。
mpc影象檔案適用於一次寫入,多次讀取模式,使用mpc將影象直接對映到記憶體,而不是每次重新讀取和解壓源影象。
3.Q8版本
ImageMagick Q16版本允許在不縮放的情況下讀寫16點陣圖像,但畫素快取消耗的資源是Q8版本的兩倍,Q8版本的執行速度通常比Q16版本要快。
畫素快取消耗 = 寬度*高度*位深度/ 8 *通道
Q8位深 = 8
Q16位深 = 16
通道 = 紅 + 綠 + 藍 + 阿爾法強度
更詳細的效能優化資訊可在ImageMagick Architecture檢視。
總結
web端實現實時圖片生成採坑挺多,目前ImageMagick的方案還有些效能瓶頸,持續優化中。換個思路,如果傳遞頁面URL,由客戶端渲染頁面,實現截圖,或許是更優的方案,目前還沒嘗試,值得一試…
引用
[1]、html2canvas (https://html2canvas.hertzen.com/features)
[2]、PhantomJS官網 (http://phantomjs.org/)
[3]、phantomjs-node (https://github.com/amir20/phantomjs-node)
[4]、node canvas官網 (https://github.com/Automattic/node-canvas)
[5]、node gm (http://aheckmann.github.io/gm/docs.html)
[6]、Twitter的彩色emoji字型 (https://github.com/eosrei/twemoji-color-font)
[7]、ImageMagick Architecture (http://www.imagemagick.org/script/architecture.php)