GIF 与 QRcode 玩具二则

GIF 与 QRcode 玩具二则

SEO

文件转GIF
文件转动图 分享
个性化二维码
彩色二维码
动图二维码
动态二维码
Animated QRcode File to GIF

搞一堆关键词蜜罐看有没有和我一样无聊的人掉进来……

链接

引子

七月九号上午在看虎扑和 V2EX 的时候,突然想到了一个点子:是否可以把文件转化为 GIF 然后放在互联网的图床上进行分享呢?

当今互联网时代,GIF 的历史使命尚未完成。由于其悠久的历史,几乎所有终端、设备、服务都会想方设法对其进行兼容。就连最赶时髦的苹果,也不得不顺从民意,在 iOS 11 加入了相册对 GIF 动图预览的支持。可以预见,在下一个一统江湖的多媒体格式到来前,GIF 仍将在互联网的舞台中留存多日。

GIF 的一大好处——各大图床为了保持最佳兼容性,一般对较小的 GIF 文件不会压缩。一些技术上较为保守的网站,如虎扑,甚至对任何 GIF 都不做处理直接存储。这便给了一些“巨大GIF”党可乘之机——在虎扑上,有个叫 iameasy81 的用户疯狂上传 400*400@50fps 的巨型 GIF 文件,动辄二三十兆,每次加载时都让我怀疑人生,还以为中美光缆阻塞了。

当然,大多数平台不太愿意处理 GIF 文件,而是选择直接存储的原因,在于 GIF 优化是一个复杂且耗费算力的问题。由于 GIF89a 标准最初设计上较为简单,并没有类似 AVC 这样可以轻松指定画面质量、文件大小的特性,因而想将一张 GIF 压缩到指定尺寸,实际上是一个类似于猜+试的过程。同时,因为 GIF 标准存在许多变数,如局部色板、抖动、色板继承、颜色丢弃、透明色映射等等问题,倘若压缩程序写得不好,很有可能反而把一张精心优化过的 GIF 越压越大。因此,许多图床和文件存储选择睁一只眼闭一只眼,选择直接不修改存储 GIF,而这便是令我产生这个想法的一大原因。

事不宜迟,本想先把手头的活干完再来做这件事,但越想越觉得有意思且很容易实现,便立即开始写码。

二维码

二维码是日本人在九十年代发明的,最初为了提高工业工程的效率,后来演变为我们在电子支付、内容分享中日常用到的样子。二维码的本质就是将字符串转为一张图像,然后接收端再通过模式识别解码图像,得到原来的字符串。二维码诞生之初,便有了许多复杂的设计,如版本号,对应二维码的矩阵大小,从 21 * 21 到 177 * 177;定位图样,方便模式识别时确定旋转和对齐方向;纠错等级,分别有低、中、高、最高。而且,最好的是——二维码的编码是定长码,这样一来,等长字符串编码生成的二维码尺寸一定相同。为了找到一个合适的矩阵尺寸,我先用一些在线二维码生成器进行了简单的试验。

128个零

当我用全零字符串填充二维码时,如上图一样奇特的现象出现了——二维码的右半部分出现了规则的明暗相间的方块,好像一个棋盘。这是容易解释的,因为 0 对应的编码较为特殊,重复的相同字符造成了重复的图案。

看到这个二维码,我突然想到之前见过的许多异形二维码,如三维棱台、圆角、艺术化等变化。上 GitHub 一搜,果然让我找到很多现成的库。其中,以 Halftone QRCodes 和 QArt 二维码最为抓人眼球。前者能够生成抖动的黑白 GIF二维码,且每一帧都能扫出相同的内容;后者能够将彩色图片作为背景嵌入二维码,也能支持动态图。事不宜迟,我便先开始尝试做动态二维码。

个性二维码生成

膜 + 孙辰表示不服 = 孙辰表示不服

重构了 CuteR 这个对 qrcode 进行封装的 Python 库。改进了坐标变换与并行执行的能力。通过给定输入字符串与输入的背景 GIF,即可得到输出的动态二维码。输出后再通过 gifsicle 进行优化压缩,得到最终的图像。示例参照上面的图像。

文件转二维码 GIF

逻辑

  • 编码

首先将文件转为对应的 base64/base32 字符串表示,然后将字符串按照 150 字符左右的长度分割,并将每个分割后的子串编码为二维码。接着把二维码转为 GIF 的一帧,并拼接得到输出 GIF。

  • 解码

将 GIF 文件按帧读取,对每一帧进行二维码解码,再将解码后得到的字符串进行拼接,并 base64/base32 解码得到文件的二进制表达,存储到相应路径。

改进

逻辑比较清晰和简单,改进主要存在于几方面。

  1. 输出 GIF 的大小

    由于 PIL 库默认的 GIF 支持很差,经过编码,13KB 的原文件竟然变成了 500KB 的 GIF。这样的倍率显然是不可以接受的。仔细一想,GIF 每一帧都是二维码,最简单的二维码应该只有黑白两种颜色,且其最小尺寸可以设置为二维码每一小方格为 1 像素时对应的尺寸。如此一来,我可以按照这个原则编码出每一帧都完全符合 GIF 规范时最小的数据帧。

    将 GIF 设为全局色板模式,且色板只有两个颜色。每一帧的间隔相等,取消单帧色板,并将 LZW 压缩中的码长也设为最小的 2bits。这样,生成的 GIF 文件称得上几近最小。当然,通过取消帧间丢弃的方法或许还能减小尺寸,但目测很有可能产生副作用——因为多加一种透明色相当于所有索引值都必须多 1bit,忽略被头部中丢弃方法、延迟时间字段占据的空间,理论上也只有两帧间变化不到 50% 时才可能有提升。

    基于之前项目中的 GIF 编码器,为这个 GIF 输出渲染重新定制了一个尽可能优化的版本。默认输出为二维码线宽 1px 的图像,在解码时再用最近邻方法放大为 2 倍以方便识别。

    测试结果:

    原文件 仅优化 GIF 渲染 未优化
    111284 (110KB) 387615 (388KB) 2149022 (2.2MB)
  2. 线性改为并行

    多帧图像处理是天生就适用于多线程的场景之一。如果不计帧间关系,每一帧都是独立的问题;就算考虑帧间关系,也只用同时处理两帧。将字符串通过 Python 的 multiprocessing 库异步生成二维码矩阵后渲染为图像,然后最后装配,能够大幅提升编码速度。

    测试结果:

    对于同一个 111KB 原文件的测试结果,共生成 1238 帧:

    并行 线性
    10.25s 39.18s
  3. 每帧字符串长度与字符串码表

    根据二维码的标准,大于等于 version 7 的二维码需要预留区域存放版本信息。因此,为了使输出 GIF 文件最小,应尽可能在每一帧中装入最多的字符,且选择 version 6 或最大的 version 40。

    二维码的数字和英文字符码长不等,而 base64 编码后的字符串数字和字符的分配与二进制文件的字节有关,并不固定。根据参考资料中的数据来看,官网给出的数字+字母的数据是 195。而我实测只能放进 134 个字符。看了下 qrcode 库的源码,原来二维码的数字+字母码表定义为大写字母和数字以及几个特殊符号,共45位的码表。按照给的数据计算,如果是数字+大写英文字符放进 195 个,而字节放进 134 个的话,相对能多放 45% 的字符;而二进制字节编码为 base64 后只增加 33%,似乎是一个不错的 trade-off。只是这种情况并不能使用 base64,最理想的情况是自己写一个 base45 的编码解码器,网上也有人这么做过;另一种是直接用现成的 base32 编解码器。根据二维码的特点,存在三种可能:数字编码,如 base8 或 base10;数字-大写字母编码,如 base32;二进制编码。我估计后两种的编码效率更高些。

    目前,我把 base64 和 base32 的编码方式都实现了。相对而言 base32 的效率稍高。参见下表的效率对比。两种编码方式均为最优 GIF 渲染。

    原文件 111284 (110KB) base32 base64
    version 6 311562 (312KB) 377123 (377KB)
    version 40 204870 (205KB) 248118 (248KB)

    可见,目前最优情况为 version 40 与 base32 编码搭配。此时,文件大小相对于原文件为 1.84 倍。考虑到 base32 编码效率约为 1.6 倍(5 字节转化为 8 字节),二维码最低纠错 7% 以后,理论上可以得到的最小值已经是 1.712 倍。这样看来,整个方法的各部分表现已经较为优秀。写了半天忍不住自夸

总结

测试文件 110KB -> 编码后GIF 205KB

总体而言,若想实现这种转换,且不考虑对于人所持设备的兼容性的话,使用最大的二维码版本与最低的纠错参数,可以达到编码后文件体积膨胀率最低。如果需要考虑中途由人来识别,如设计一个 app 录像得到所有帧,再在手机里转为文件的话,尽量使被录制的二维码尺寸为较小的版本,可以更好保证识别率。

最开始想到这个想法的时候,并不确定其可行性有多少。因为平时一直觉得 GIF 本来就挺大了,再加上二维码的冗余以及定位孔占据的位置,最后的结果一定不理想。结果编写测试完毕后,发现还是有一定可用性的。例如对于网上最常见的 5MB 图床来讲,将近可以放下两张软盘所能存储的内容,还是很可观的——要知道,哪怕到现在,我自己写过的所有文本文件加起来也只是在兆的量级而已。这个博客上所有的文章源文件加起来也只有一张软盘的容量……

这个项目到此就告一段落了。目前看起来没有什么太大的用处,因为自己不会 js,没法把这样的编码解码器部署到前端供别人使用。同时,这种工具得有一定的用户规模,一传十十传百才能发挥实力。然而,如果这个工具流行起来,估计许多图床就会改变策略,禁止纯二维码动图的上传了,毕竟这种功能是游走在用户协议边缘的灰色地带的。

写了一天代码,查了一天文档,自认为整体做得还算满意。未来有空封装为二进制文件。

有没有一点黑客帝国的感觉

字节跳动

参考

Chen Ting

Chen Ting

The page aimed to exhibit activities & achievements during Ting's undergraduate & graduate period. Meanwhile, other aspects such as lifestyles, literary work, travel notes, etc. would interweave in the narration.

Leave a Comment

Disqus might be GFW-ed. Excited!