前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >web实时长图实践

web实时长图实践

作者头像
QQ音乐技术团队
发布于 2018-03-01 06:11:32
发布于 2018-03-01 06:11:32
6.8K00
代码可运行
举报
运行总次数:0
代码可运行

背景简介

全民K歌专辑发布新玩法,传统宣传专辑战绩的流程,从获取数据,到制作海报,到传播,周期长运营成本高,如何快速分享战绩进行荣誉感的传播成为一个亟待解决的问题。

产品:能不能在专辑大事件触发时,自动生成一个大事件长图,供粉丝分享传播?

开发:理论上没问题,尝试下吧…

浏览器端实现方案

开发:大事件长图和专辑详情页大事件tab的视觉效果基本一致,如果能复用可以减少开发时间。

开发:怎么复用呢?

于是便有了下面在浏览器端尝试dom转图片的两种方案:

html2canvas

html2canvas一个在浏览器端通过JS对整个或部分页面进行“截屏”的库。

html2canvas使用方法简单,截屏的核心代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let imgBase64;

html2canvas(htm,{
   onrendered : function(canvas){

    //生成base64图片数据

    imgBase64 = canvas.toDataURL();
});

使用简单,但是坑不少,遇到的坑及解决方案:

1.截图模糊

主要解决思路:

1)将canvas的width和height属性放大为2倍。

2)将canvas的CSS样式width和height设置为原先1倍的大小。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<canvas width="200" height="100" style="width:100px;height:50px;"></canvas>
2.截图不全

源码获取dom高度不准确,修改源码,获取高度后手动传,修改方式如下:

源码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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;
});

修改后

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//添加自定义高度宽度

    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方案的尝试:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//要转成图片的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时,注意安装以下依赖:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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库,实现截图的核心代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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截图是设置缩放参数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//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+,导致加载慢,设置截图质量:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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类似,这里就不列举了。

绘制图片的核心代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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++、PerlJava、PHP、Python或Ruby编程来完成。更多详情可在ImageMagick官网查看。

GraphicsMagick是从 ImageMagick 5.5.2 分支出来的,据说它变得更稳定和优秀,更多详情可在GraphicsMagick官网查看。

看起来GraphicsMagick是更好的选择,但是由于node gm这个库没有实现GraphicsMagick的半透明和圆角支持,而且针对专辑的大事件长图做了一些性能对比两者差异不大,所以选择使用ImageMagick。

node gm切换ImageMagick的方式非常简单,只要加以下设置:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var gm = require('gm');
var imageMagick = gm.subClass({ imageMagick: true });

不可避免的,使用ImageMagick也遇到一些坑:

1.半透明遮罩

设计:专辑封面背景使用白透明遮罩,遮罩的颜色根据封面图来定,深色封面图用白色文字,浅色封面图用黑色文字。

开发:OK,先canvas获取封面图颜色信息,再判断颜色深浅

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//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支持圆角)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
.fill("avatar.jpg")
.drawCircle(80,120,30,120)

ImageMagick圆角图片实现方式与canvas类似,画一个圆,然后用头像图片去填充来实现头像圆角。

3.昵称emoji表情

ImageMagick绘制昵称中的表情图比较麻烦,使用支持emoji的字体,尝试过Twitter的彩色emoji字体,但是ImageMagick有BUG,不能还原为彩色的。

最终解决方案:

1)使用等宽字体,方便计算精确的emoji位置

2)ImageMagick绘制昵称中的表情图片

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
.draw("image Over " + size + " " + url)

ImageMagick性能优化:

优化前:

优化后:

ImageMagick生成单张图片耗时100ms左右,但是并发请求多了平均耗时就暴涨到3S+,这个速度显然是不能接受的,经过一番优化后将平均耗时降到1S左右,主要优化点如下:

1.gm代码拼接,VM中执行

多次调用gm多次操作图片,严重影响性能,将图片操作代码拼接成字符串,在VM中执行,只调用一次gm,核心代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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版本要快。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
像素缓存消耗 = 宽度*高度*位深度/ 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)

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-02-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 QQ音乐技术团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
我是如何在微人事项目中提高RabbitMQ消息可靠性的?
公众号后台回复 SpringBoot,免费获取 274 页SpringBoot修炼手册。
江南一点雨
2020/03/06
7400
我是如何在微人事项目中提高RabbitMQ消息可靠性的?
RabbitMQ01-入门介绍
  前面给大家介绍过了ActiveMQ,本文开始给大家介绍下另一个消息中间件RabbitMQ。
用户4919348
2019/05/23
4660
01、RabbitMQ入门
4.创建Rabbit配置类RabbitConfig,配置类主要用来配置队列、交换器、路由等高级信息
天蝎座的程序媛
2023/10/17
3090
01、RabbitMQ入门
RabbitMQ05-交换器【fanout】介绍
  FanoutExchange 的数据交换策略是把所有到达 FanoutExchang 的消息转发给所有与它绑定的Queue ,在这种策略中, routingkey 将不起任何作用.
用户4919348
2019/05/23
5680
【RabbitMq 篇二】-RabbitMq 发送与消费
本文介绍RabbitMq各个消息类型,以及用使用Fanout 类型进行消息的发送和消费,让大家对RabbitMq有一个简单的认识。
胖虎
2019/06/26
1.2K0
【RabbitMq 篇二】-RabbitMq 发送与消费
RabbitMQ管理平台与主流MQ框架
MQ(Message Queue)消息队列,是基础数据结构中“先进先出(FIFO)”的一种数据结构。一般用来解决应用解耦,异步消息,流量削峰等问题,实现高性能,高可用,可伸缩和最终一致性架构。  从字面意思上看,本质是个队列,只不过队列中存放的内容是message而已
用户10196776
2023/10/17
4920
RabbitMQ管理平台与主流MQ框架
RabbitMQ03-交换器【direct】介绍
  DirectExchange 路由策略是将消息队列绑定到 DirectExchange 上,当 一条消息到达DirectExchange 时会被转发到与该条消息 routing key 相同的 Queue 上,例如消息队列名为“hello-queue ”,则 routingkey 为“hello-queue ”的消息会被该消息队列接收。
用户4919348
2019/05/23
6660
关于 RabbitMQ,应该没有比这更详细的教程了!
从八月份开始,断断续续连载了不少 RabbitMQ 教程,最近抽空整理了一下,未来可能会有一个视频教程,小伙伴们敬请期待。
江南一点雨
2022/01/24
1.1K0
关于 RabbitMQ,应该没有比这更详细的教程了!
RabbitMQ消息中间件从入门到高级(一)
MQ全称为Message Queue, 消息队列(MQ)是一种应用程序对应用程序的通信方法。应用程序通过读写出入队列的消息(针对应用程序的数据)来通信,而无需专用连接来链接它们。消息传递指的是程序之间通过在消息中发送数据进行通信,而不是通过直接调用彼此来通信,直接调用通常是用于诸如远程过程调用的技术。排队指的是应用程序通过 队列来通信。队列的使用除去了接收和发送应用程序同时执行的要求。其中较为成熟的MQ产品有IBM WEBSPHERE MQ等等。
用户1212940
2022/04/13
5980
RabbitMQ消息中间件从入门到高级(一)
SpringCloud-Turbine【RabbitMQ服务监控】
  前面我们介绍了通过turbine直接聚合多个服务的监控信息,实现了服务的监控,但是这种方式有个不太好的地方就是turbine和服务的耦合性太强了,针对这个问题,我们可以将服务的监控消息发送到RabbitMQ中,然后turbine中RabbitMQ中获取获取监控消息,这样就实现类服务和turbine的解耦。
用户4919348
2019/06/16
8600
Spring Boot整合RabbitMQ详细教程
场景说明:用户注册后,需要发注册邮件和注册短信,传统的做法有两种1.串行的方式;2.并行的方式 (1)串行方式:将注册信息写入数据库后,发送注册邮件,再发送注册短信,以上三个任务全部完成后才返回给客户端。 这有一个问题是,邮件,短信并不是必须的,它只是一个通知,而这种做法让客户端等待没有必要等待的东西.
全栈程序员站长
2022/08/27
6700
Spring Boot整合RabbitMQ详细教程
Spring Boot中使用RabbitMQ
复刻一篇老文,为后续要发的内容做一些铺垫 Message Broker与AMQP简介 Message Broker是一种消息验证、传输、路由的架构模式,其设计目标主要应用于下面这些场景: 消息路由到一个或多个目的地 消息转化为其他的表现方式 执行消息的聚集、消息的分解,并将结果发送到他们的目的地,然后重新组合相应返回给消息用户 调用Web服务来检索数据 响应事件或错误 使用发布-订阅模式来提供内容或基于主题的消息路由 AMQP是Advanced Message Queuing Protocol的简称,它是一
程序猿DD
2018/02/01
1.2K0
Spring Boot中使用RabbitMQ
RabbitMQ与SpringBoot整合
RabbitMQ是消息中间件的一种,消息中间件即分布式系统中完成消息的发送和接收的基础软件.这些软件有很多,包括ActiveMQ(apache公司的),RocketMQ(阿里巴巴公司的,现已经转让给apache).
Java编程指南
2019/11/25
5750
RabbitMQ在微服务中
RabbitMQ,也称为开源消息代理,支持多种消息协议,可以部署在分布式系统上。它非常轻巧,可以轻松部署应用程序。它主要作为一个队列,首先可以对输入的消息进行操作。RabbitMQ可在许多操作系统和云环境中运行,并为大多数流行语言提供各种开发人员工具。它是生产者 - 消费者风格模式,生产者发送消息,消费者使用它。RabbitMQ的主要功能如下:
February
2018/11/27
1.9K0
RabbitMQ之发布确认高级
  在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败, 导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢? 特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢:
别团等shy哥发育
2023/02/25
5510
RabbitMQ之发布确认高级
RabbitMQ04-交换器【topic】介绍
  TopicExchange 是比较复杂也比较灵活的 种路由策略,在TopicExchange 中,Queue 通过routingkey 绑定到 TopicExchange 上,当消息到达 TopicExchange 后,TopicExchange 根据消息的routingkey 消息路由到一个或者多 Queue上,相比direct模式topic会更加的灵活些。
用户4919348
2019/05/23
4730
14. Springboot集成RabbitMQ
消息队列(Message Queue,简称 MQ)是一种异步的消息传递中间件,它解耦了应用程序之间的通信。应用程序可以将消息发送到队列,而无需知道谁会接收这些消息。接收应用程序可以从队列中检索消息,而无需知道谁发送了这些消息。消息队列是一种重要的中间件,它可以帮助应用程序之间进行异步、可靠、可扩展的通信。常见的消息队列中间件有ActiveMQ,RabbitMQ,Kafka......今天我们就来介绍RabbitMQ。
有一只柴犬
2024/03/28
2070
14. Springboot集成RabbitMQ
RabbitMQ消息路由失败的处理方案(回调与备份交换机AE)
我们知道,消息在RabbitMQ的整个生命周期是生产者投递消息到Exchange,Exchange根据路由键将消息路由到合适的Queue,Queue再将消息推(或消费者主动拉)给消费者。
喜欢天文的pony站长
2021/03/05
1.2K0
RabbitMQ消息路由失败的处理方案(回调与备份交换机AE)
Spring cloud stream【入门介绍】
案例代码:https://github.com/q279583842q/springcloud-e-book
用户4919348
2019/07/03
1.1K0
Spring cloud stream【入门介绍】
【RabbitMq 篇三】-备份交换器
备份交换器也叫备胎交换器,顾名思义就是,替代现任的,分手后可以及时上位,让你的爱情持续下去,原理就是我爱你换不来你爱我,那我就去找备胎了。回到消息里就很好理解了,生产者发送消息,由于路由错误不能到达指定队列,所以就路由到备胎队列消费,这样做可以保证未被路由的消息不会丢失,其实保证消息不会丢失还可以通过消息的回调方法,添加ReturnListener的编程逻辑,但是这样做生产者的代码会复杂写,所以我们使用备份交换器实现。
胖虎
2019/06/26
7590
【RabbitMq 篇三】-备份交换器
相关推荐
我是如何在微人事项目中提高RabbitMQ消息可靠性的?
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文