背景簡介css
全民K歌專輯發佈新玩法,傳統宣傳專輯戰績的流程,從獲取數據,到製做海報,到傳播,週期長運營成本高,如何快速分享戰績進行榮譽感的傳播成爲一個亟待解決的問題。html
產品:能不能在專輯大事件觸發時,自動生成一個大事件長圖,供粉絲分享傳播?node
開發:理論上沒問題,嘗試下吧…linux
開發:大事件長圖和專輯詳情頁大事件tab的視覺效果基本一致,若是能複用能夠減小開發時間。ios
開發:怎麼複用呢?c++
因而便有了下面在瀏覽器端嘗試dom轉圖片的兩種方案:web
html2canvas一個在瀏覽器端經過JS對整個或部分頁面進行「截屏」的庫。sql
html2canvas使用方法簡單,截屏的核心代碼以下:編程
let imgBase64; html2canvas(htm,{ onrendered : function(canvas){ //生成base64圖片數據 imgBase64 = canvas.toDataURL(); });
使用簡單,可是坑很多,遇到的坑及解決方案:canvas
主要解決思路:
1)將canvas的width和height屬性放大爲2倍。
2)將canvas的CSS樣式width和height設置爲原先1倍的大小。
<canvas width="200" height="100" style="width:100px;height:50px;"></canvas>
源碼獲取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; });
截圖慢得從html2canvas的原理提及,html2canvas並非真正的截圖,而是遍歷加載的頁面DOM,收集全部元素的信息,而後基於從DOM讀取的屬性使用canvas來繪製。
基於這個截圖原理,慢的問題優化空間不大,並且html2canvas還有些CSS的限制,它只能正確地呈現它支持的CSS屬性,完整的CSS屬性支持列表,能夠在官網查看。
關於慢,最簡單的解決方案是在用戶操做前提早生成截圖。
html2canvas截圖後,將圖片的base64傳到客戶端的分享組件,當base64超過500k可能致使客戶端卡死或crash,若是慢的問題還能忍,那這個問題是真的無法接受的。
除了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方案無法繞過的坑:
因爲安全限制,ios下跨域圖片加crossOrigin
屬性也無法繞過跨域問題。
和html2canvas同樣,svg轉圖片後最終也是轉base64傳分享組件,base64超過500K可能致使的卡死和crash問題也存在。
開發:瀏覽器端的方案crash問題不能忍,不如在服務器端生成圖片,傳圖片URL到分享組件?
本着最大限度複用代碼的初衷,首選了無頭瀏覽器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遇到的坑也很多,主要是環境問題:
開發:在mac上和windows上生成截圖正常,部署到測試環境後不能生成截圖,打印PhantomJS日誌,沒有明確的報錯信息。linux下權限問題?
查看PhantomJS和目錄權限,PhantomJS沒有寫權限,修復權限問題,圖片仍然不能生成。
開發:字母命名的截圖正常生成,不支持圖片文件名包含數字?
一番驗證,截圖名包含數字phantomjs-node不能正常生成圖片文件。
開發:顏色和圖案均可以渲染到截圖中,只有文字不能渲染,字體有問題?
確認測試機中字體目錄爲空,更新字體,文字終於能正常渲染到截圖中。
又是模糊問題…
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);
模糊問題設置2倍圖後,圖片大小暴漲到6M+,致使加載慢,設置截圖質量:
page.render(fileName,{quality:85}).then((err) => { phInstance.exit(); })
PhantomJS生成一個最簡單的截圖,耗時2S左右,這個速度顯然是不能接受的,暫時沒找到比較好的優化方式。
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是一套功能強大、穩定並且免費的工具集和開發包,能夠用來讀、寫和處理超過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也遇到一些坑:
設計:專輯封面背景使用白透明遮罩,遮罩的顏色根據封面圖來定,深色封面圖用白色文字,淺色封面圖用黑色文字。
開發: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)")
設計:這些頭像要用圓角哦。
開發:OK(還好ImageMagick支持圓角)
.fill("avatar.jpg") .drawCircle(80,120,30,120)
ImageMagick圓角圖片實現方式與canvas相似,畫一個圓,而後用頭像圖片去填充來實現頭像圓角。
ImageMagick繪製暱稱中的表情圖比較麻煩,使用支持emoji的字體,嘗試過Twitter的彩色emoji字體,可是ImageMagick有BUG,不能還原爲彩色的。
最終解決方案:
1)使用等寬字體,方便計算精確的emoji位置
2)ImageMagick繪製暱稱中的表情圖片
.draw("image Over " + size + " " + url)
ImageMagick性能優化:
優化前:
優化後:
ImageMagick生成單張圖片耗時100ms左右,可是併發請求多了平均耗時就暴漲到3S+,這個速度顯然是不能接受的,通過一番優化後將平均耗時降到1S左右,主要優化點以下:
屢次調用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);
mpc是ImageMagick提供的一種持久高速緩存格式,減小對圖像格式進行解碼和編碼像素的開銷。
mpc生成兩個文件:
1)一個擴展名.mpc保留了與圖像或圖像序列相關的全部屬性(例如寬度,高度,色彩空間等)。
2)一個擴展名.cache,是本地原始格式的像素緩存。
讀取mpc圖像文件時,ImageMagick讀取圖像屬性,並將內存映射到磁盤上的像素緩存,無需解碼圖像像素,不過mpc的文件大小比其餘圖像格式大。
mpc圖像文件適用於一次寫入,屢次讀取模式,使用mpc將圖像直接映射到內存,而不是每次從新讀取和解壓源圖像。
ImageMagick Q16版本容許在不縮放的狀況下讀寫16位圖像,但像素緩存消耗的資源是Q8版本的兩倍,Q8版本的執行速度一般比Q16版本要快。
像素緩存消耗 = 寬度*高度*位深度/ 8 *通道 Q8位深 = 8 Q16位深 = 16 通道 = 紅 + 綠 + 藍 + 阿爾法強度
更詳細的性能優化信息可在ImageMagick Architecture查看。
web端實現實時圖片生成採坑挺多,目前ImageMagick的方案還有些性能瓶頸,持續優化中。換個思路,若是傳遞頁面URL,由客戶端渲染頁面,實現截圖,或許是更優的方案,目前還沒嘗試,值得一試…