最近有个需求,根据前台用户输入的内容在后台生成宣传图。

我的方法是,把内容渲染成一个 HTML 页面,然后使用 wkhtmltoimage 把网页转换成图片。遇到了一个问题:用户输入的内容带有 Emoji 表情符号,由于 Linux 服务器上没有对应的字体问题,生成图片的时候,Emoji 表情符号全变成了无法识别的框框。

当时我想,要是能把 iOS 系统的 Emoji 表情字体文件 Copy 过来用,那就完美了。在网上搜罗了很久,最终无功而返, iOS 系统的字体文件不能直接在其他系统上使用!

最后,我打算用最笨的方法,就是把 Emoji 表情符号替换成 PNG 图片来显示。即把原来的 Emoji 符号替换成 HTML 的 img 标签。

在 https://www.unicode.org/ 找到了所有 Emoji 表情对应的图片。两个页面,每个页面大概 25MB,打开很慢的,地址分别是:

两个页面的内容格式如下:

这两个页面都列举了每个 Emoji 表情的 Unicode 编码,以及各厂商自己实现的表情效果(图),查看源代码可以发现表情图片是直接嵌入的 base64 数据。我们要把 Code 和 Appl 这两列的数据保存下来,并做一个映射关系。

下面是我整理的 PHP 脚本:

<?php

set_time_limit(0);

$files = [
    'full-emoji-list.html'      => 'https://www.unicode.org/emoji/charts/full-emoji-list.html',
    'full-emoji-modifiers.html' => 'https://www.unicode.org/emoji/charts/full-emoji-modifiers.html'
];

$path = 'emoji/';

$arr = [];

if (!is_dir($path)) {
    mkdir($path);
}

foreach ($files as $file => $url) {
    if (!is_file($file)) {
        echo "正在下载文件 $file ...\n";
        $opts = [
            'http' => [
                'method'  => 'GET',
                'timeout' => 1800
            ],
        ];
        $context = stream_context_create($opts);
        $str = @file_get_contents($url, false, $context);
        if (stripos($str, '</html>') === false) {
            echo "下载失败,建议您手动下载保存到本目录。\n";
            exit;
        }
        file_put_contents($file, $str);
    } else {
        $str = file_get_contents($file);
    }

    $pattern = "#<tr><td class='rchars'>(\d+)</td>
<td class='code'><a href='(?:.+?)' name='(?:.+?)'>(.+?)</a></td>
<td class='chars'>(?:.+?)</td>
<td class='andr(?: alt)?(?: miss)?'>(?:—|<img alt='(?:.+?)' class='imga' src='(.+?)'>)</td>#";

    preg_match_all($pattern, $str, $m);

    foreach ($m[1] as $k => $v) {
        $num = $v;
        $code = $m[2][$k];
        $data = $m[3][$k];

        if (!$data) {
            continue;
        }

        // 代码格式转换,如 "U+1F9D1 U+200D U+1F3A8" 转换成 "1F9D1-200D-1F3A8"
        $code = str_replace(['U+', ' '], ['', '-'], $code);
        $filename = $code . '.png';
        $data = str_replace('data:image/png;base64,', '', $data);
        $data = base64_decode($data);
        file_put_contents($path . $filename, $data);
        echo "已保存 $filename\n";

        $arr[] = $code;
    }
}

usort($arr, function ($a, $b) {
    return strlen($b) <=> strlen($a);
});

$result = json_encode($arr, JSON_PRETTY_PRINT);
$data = '<?php' . "\n" . 'return ' . $result . ';';
file_put_contents('emoji.php', $data);
echo "已生成 emoji.php\n";

将以上代码保存为 index.php(文件为 UTF-8 编码 UNIX 风格换行符),然后在命令行模式下执行 php index.php。执行完毕,会在该目录下生成 emoji.php 文件,表情图片保存在 ./emoji 子目录里(3000多个图片)。文件 emoji.php 定义了一个数组,数组元素加上后缀名 .png,就是 ./emoji 目录下表情图片的文件名。

文件 emoji.php 的内容大致如下:

<?php
return [
    "1F468-200D-2764-FE0F-200D-1F48B-200D-1F468",
    "1F469-200D-2764-FE0F-200D-1F48B-200D-1F468",
    //...此处省略很多行...
    "1F3F4-E0067-E0062-E0077-E006C-E0073-E007F",
    //...此处省略很多行...
    "1F468-200D-2764-FE0F-200D-1F468",
    //...此处省略很多行...
    "270C-1F3FE",
    //...此处省略很多行...
    "264F",
    "2651",
    "2197"
];

Emoji 表情比较特殊,所占的字节长短不一,有些很长,有些很短。我把最长的放在前面,因为后面我打算用 PHP 的 preg_replace 函数进行批量替换。如果把短的放最前面,替换时会错乱。

替换语法:

// 举例 Emoji 编码 1F9D1-200D-1F3A8 的替换
$pattern = '/\x{1F9D1}\x{200D}\x{1F3A8}/u';
$replace = '<img src="./emoji/1F9D1-200D-1F3A8.png" />';
$html = preg_replace($pattern, $replace, $html);

写个测试脚本 test.php:

<?php

function emoji2image($html) {
    $emojiCodes = require './emoji.php';

    $pattern = [];
    $replace = [];

    foreach ($emojiCodes as $code) {
        $pattern = '/\x{' . str_replace('-', '}\x{', $code) . '}/u';
        $imageUrl = '<img class="emoji" src="./emoji/' . $code . '.png" />';
        $pattern[] = $pattern;
        $replace[] = $imageUrl;
    }

    $html = preg_replace($pattern, $replace, $html);

    return $html;
}

$html = '这是😀🤦🏻‍♂️🌚什么?';
$html2 = emoji2image($html);

?>
<!doctype html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>test</title>
        <style>
            p {
                font-size: 14px;
                line-height: 150%;
            }
            .p1 {
                font-size: 18px;
            }
            .p1 .emoji {
                width: 20px;
                height: 20px;
                padding: 0 1px 4px;
                vertical-align: bottom;
            }
            .p2 {
                font-size: 32px;
            }
            .p2 .emoji {
                width: 40px;
                height: 40px;
                padding: 0 1px 4px;
                vertical-align: bottom;
            }
        </style>
    </head>
    <body>
        <p>原内容:<?php echo $html; ?></p>
        <p class="p1">字号18px:<?php echo $html2; ?></p>
        <p class="p2">字号32px:<?php echo $html2; ?></p>
    </body>
</html>

页面展示效果如下: