By Yuanyue.韦
理论上所有聊天时能用的小图表情(绘文字)都算是 emoji,本文所说的 emoji 是指 Unicode 存在对应编码的系统内置的的小图表情。
在我们平时输入的文本中,emoji十分常见,但在显示和编码上它却是一个幺蛾子般的存在。
它诡异就诡异在:
- 每一个设备对于 emoji 的实现差异性太大:不仅和系统有关,还和系统版本、系统的某个补丁、系统内的某些字体、浏览器品牌、浏览器版本都有关系,所以可以这么说,不到浏览器渲染完成的最后一刻,你都没办法判断一个 emoji 是否在当前页面内被支持。
- emoji 的编码问题:首先,emoji在 unicode 中的分布非常零散。网上能搜到很多所谓能替换字符串中 emoji 的正则表达式,它们实现的原理都是在 unicode 中圈定了一个范围,这个范围是大部分 emoji 所在的区域(大多落在U+1f300 - U+1f9cf),以此来区分是否是 emoji。但这样的方式只能选出部分 emoji,因为emoji 在 unicode 中是随着 unicode 版本的增加而不断增加的,“地盘”也不断扩大。从 Unicode 的官网(http://www.unicode.org/charts/PDF/ )和维基百科(https://en.wikipedia.org/wiki/Emoji )中我们也可以看到 U+2030 - U+3000 和其他零散的地方其实也穿插着包含了不少 emoji。不仅如此,它的编码也不仅仅只有2或4字节,因为有的 emoji 还包含零宽连字(zero-width joiner),比如一家四口人的“👩👩👦👦”,它就有25字节,UTF16编码是“1f469-200d-1f469-200d-1f466-200d-1f466”,可以看出是由四个4字节的人脸 emoji + 三个零宽连字符(U+200d)组成。可以认为,emoji 的字节数是无上限的,万一哪天出了一个 AKB48 全体成员的 emoji 呢?太多的情况普通的正则不能匹配出来。
emoji 这些诡异的特性给开发者们带来不少问题,比如:
- MySQL 5.5.3以下的版本用UTF8编码存储最多只支持3字节,然而 emoji 这货哪怕忽略零宽连字,多数都有4字节。
- 要怎么判断当前页面上哪些 emoji 能正常显示?毕竟把页面上所有 emoji 无脑替换成小图片太不优雅了,能用系统自带的 emoji 也没必要多一个网络请求去载入图片。
对于第一个问题,升级 MySQL 并且用CHARACTER SET utf8mb4把字符集设置为UTF8mb4是最合适的处理方法,否则会出现在 emoji 处字符串被截断或存入数据库后全变成了“???”。
对于第二个问题,google 和 stackoverflow 上搜了一大圈都没找到和我有相同疑问的开发者,所以在群里讨论后想了一个方法,综合考虑了以下几个方面:
- Modernizr 判断是否支持 emoji 使用的是在 canvas 上打印出一个考拉🐨的 emoji(Modernizr只是检测浏览器特性,不对某个 emoji 做针对性判断),通过判断画布上是否依然为空白画布来区分。这个方法的弊端是,有的系统对于不支持的字符显示的并非是空白,而是一个方框,也是可以在 canvas 上画出来的;其次是效率堪忧,每一次需要遍历长度为(devicePixelRatio*12 - 1)^2的数组,即121或529个像素,讲真对 canvas 做 getImageData 操作效率上并不高。
- Twitter开源了一个关于 emoji 的库:twemoji,为了统一各个平台对 emoji 的显示,把所有 emoji 都替换成小图片。它用了一个巨大无比的list(https://github.com/twitter/twemoji/blob/gh-pages/twemoji.js#L236 )把所有 emoji 正则匹配出来。这需要有人一直不断去维护这个list让它和最新的 unicode 保持同步。
- 群里某位穿女装成名的前端工程师提出,emoji 的一个特性是作为一张图片它不能用代码上色,考虑采取对 canvas 上的 emoji 做两次 fillStyle的方法,判断前后像素的 RGBA 是否完全相同,相同则为 emoji。这个方法的弊端是,并不是所有系统的 emoji 都是图片,有的系统里 emoji 就是一种字体,是可以被上色的。
综上,现在给出一种解决方案,在之前这位穿女装成名现在在度蜜月没办法碰代码的前端工程师考虑的基础上改进,主要利用了在大部分系统下emoji不能被上色的原理,对于那些 emoji 可以被上色的平台做降级处理,在2*2的 canvas 上做像素比对。
这个方法对于用字体显示 emoji 并且对不识别的字符会显示方框或问号的平台不能准确区分,但这样的平台是相当罕见的,至少目前还没有见到过。
const getTextFeature = (text, color) => {
try {
const canvas = document.createElement('canvas');
/*
因为进行scale以后的图案区域实际上不能确定,
理论上应该只在(0,0,1,1),但有的也会在它周围的像素里,
综合效率的考虑,给一个2*2的范围是比较合适的;
*/
canvas.width = 2;
canvas.height = 2;
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.font = '100px sans-serif';
ctx.fillStyle = color;
ctx.scale(0.01, 0.01);
ctx.fillText(text, 0, 0);
const imageData = ctx.getImageData(0, 0, 2, 2).data;
// 在一些系统里Uint8ClampedArray不支持常规的数组方法,需要转换一下
const imageDataArr = [];
for (let i = 0; i < imageData.length; i++) {
imageDataArr[i] = imageData[i];
}
return imageDataArr.reduce((a, b) => (a + b), 0) > 0 ?
imageDataArr.toString() : false;
} catch (e) {
return false;
}
};
const distribute = (text, mode) => {
const feature = getTextFeature(text, '#000');
return mode ? (feature && feature === getTextFeature(text, '#FFF'))
: feature;
};
const ifEmoji = (text) => {
/*
用一个最悠久而常见 emoji 来判断当前系统是使用图片还是字体来显示 emoji,
若是图片则去做上色比对,否则只对可见性做判断。
*/
const mode = distribute('😁');
return distribute(text, mode);
}
export default ifEmoji;
Usage:
ifEmoji('蛤') // => false
ifEmoji('🐸') // => If your system / browser supports this emoji character correctly, the returned value will be true.
推荐阅读:
http://crocodillon.com/blog/parsing-emoji-unicode-in-javascript