如何判断当前浏览器是否支持某一个emoji

By Yuanyue.韦

理论上所有聊天时能用的小图表情(绘文字)都算是 emoji,本文所说的 emoji 是指 Unicode 存在对应编码的系统内置的的小图表情。

在我们平时输入的文本中,emoji十分常见,但在显示和编码上它却是一个幺蛾子般的存在。

它诡异就诡异在:

  1. 每一个设备对于 emoji 的实现差异性太大:不仅和系统有关,还和系统版本、系统的某个补丁、系统内的某些字体、浏览器品牌、浏览器版本都有关系,所以可以这么说,不到浏览器渲染完成的最后一刻,你都没办法判断一个 emoji 是否在当前页面内被支持。
  2. 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 这些诡异的特性给开发者们带来不少问题,比如:

  1. MySQL 5.5.3以下的版本用UTF8编码存储最多只支持3字节,然而 emoji 这货哪怕忽略零宽连字,多数都有4字节。
  2. 要怎么判断当前页面上哪些 emoji 能正常显示?毕竟把页面上所有 emoji 无脑替换成小图片太不优雅了,能用系统自带的 emoji 也没必要多一个网络请求去载入图片。

对于第一个问题,升级 MySQL 并且用CHARACTER SET utf8mb4把字符集设置为UTF8mb4是最合适的处理方法,否则会出现在 emoji 处字符串被截断或存入数据库后全变成了“???”。

对于第二个问题,google 和 stackoverflow 上搜了一大圈都没找到和我有相同疑问的开发者,所以在群里讨论后想了一个方法,综合考虑了以下几个方面:

  1. Modernizr 判断是否支持 emoji 使用的是在 canvas 上打印出一个考拉🐨的 emoji(Modernizr只是检测浏览器特性,不对某个 emoji 做针对性判断),通过判断画布上是否依然为空白画布来区分。这个方法的弊端是,有的系统对于不支持的字符显示的并非是空白,而是一个方框,也是可以在 canvas 上画出来的;其次是效率堪忧,每一次需要遍历长度为(devicePixelRatio*12 - 1)^2的数组,即121或529个像素,讲真对 canvas 做 getImageData 操作效率上并不高。
  2. Twitter开源了一个关于 emoji 的库:twemoji,为了统一各个平台对 emoji 的显示,把所有 emoji 都替换成小图片。它用了一个巨大无比的list(https://github.com/twitter/twemoji/blob/gh-pages/twemoji.js#L236 )把所有 emoji 正则匹配出来。这需要有人一直不断去维护这个list让它和最新的 unicode 保持同步。
  3. 群里某位穿女装成名的前端工程师提出,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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容