<feGaussianBlur>
和 CSS 自带的 blur
一样,表示对图片进行指定半径的模糊处理。而 <feComponentTransfer>
则是对图片的每个像素的 RGBA 通道颜色值进行映射(就像 Photoshop 里的“曲线”一样),这里的设定是将 A 通道固定为 1(范围为 0-1),也就是使整个图像变得不透明。
给这个 SVG 滤镜任意添加一个 ID(可以短到只有一个字符),然后压缩,编码成使用 URL 编码的 Data URL,再像 HTML 中使用 #
指向有某个 ID 的元素一样引用这个滤镜,写入到 CSS 的 filter
中,就可以得到下面的 CSS 代码:
filter: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='$'%3E%3CfeGaussianBlur stdDeviation='9'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='1 1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3C/svg%3E#$") |
这是目前这个主题正在对文章封面图的使用模糊方法。看上去似乎也没什么问题,但是在写这篇的时候我尝试对上面的演示中的缩略图使用,结果在图片边框出现了严重的色彩溢出现象,溢出的部分甚至还超出了图片的显示区域……
那为什么 CSS-Tricks 那边的使用以及对这个主题的封面图使用就没有问题呢?因为那两个情况下的图片都是通过 background-image
显示的,但是这里使用的是 <img>
,稍微有点区别。至于为什么在 <img>
中使用就会在显示区域之外出现色彩溢出的部分,我也无法解释…… (;-_-)
后来我又找到了一个 Stack Overflow 上的回答,给出了另一个 SVG 模糊滤镜:
<svg xmlns="http://www.w3.org/2000/svg"> |
<feColorMatrix>
是使用变换矩阵处理每个像素的 RGBA 通道颜色值,上面的滤镜中的矩阵相当于将 A 通道的颜色值乘上 9,其他通道不变,对于被模糊(实际上是半透明)的边缘位置来说基本上就和设成 1 差不多了,并且这种实现方法不会在显示区域之外再出现什么奇怪的东西。对应的 CSS 代码:
filter: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='$'%3E%3CfeGaussianBlur stdDeviation='10'/%3E%3CfeColorMatrix type='matrix' values='1 0 0 0 0,0 1 0 0 0,0 0 1 0 0,0 0 0 9 0'/%3E%3CfeComposite in2='SourceGraphic' operator='in'/%3E%3C/filter%3E%3C/svg%3E#$"); |
用这个滤镜替代最初的演示中使用的 CSS 自带的模糊滤镜:
这样的效果就很完美了!( つ•̀ω•́)つ
CSS-Tricks 的文章还提到了 Facebook 技术团队的另一篇文章,介绍他们是如何在自家的 APP 中实现图片渐进式加载,使用的方法仍然是先立即显示模糊的缩略图再展示完整图片,但是他们的目标是将缩略图的大小压到 200 B 左右。为此他们使用了相同的质量和Huffman 表等参数进行压图,这样得到的图片就有了固定的标头,只要传输剩下的部分,在客户端再和固定的标头拼接起来,就可以得到使用的缩略图了。
So the final format became one byte for version number, one byte each for width and height, and finally the approximately 200 byte payload. The server would just send this format as part of the GraphQL response, and then the client could simply append the JPEG body to the predefined JPEG header, patch the width and height, and treat it as a regular JPEG image.
After the standard JPEG decoding, the client could run the predetermined Gaussian blur and scale it to fit the window size.
最终的数据格式先是各占 1 B 的版本号和图片的宽度和高度,剩下的部分就是 200 B 左右的负载。服务端只需要从 GraphQL 响应中返回这些数据,客户端简单地将负载拼接到预定义的 JPEG 头部后面,再填上宽度和高度,就可以当成一般的 JPEG 图片使用了。
在进行 JPEG 图片解码后,客户端就可以对图片添加高斯模糊并把它缩放到需要的大小。
自己实现图片渐进式加载的时候当然不一定要做如此极端的优化,制作缩略图时,用任意一个软件把图片的宽度和高度压到 40px 左右,控制压缩 JPEG 的质量使大小在 2 KB 以内其实就可以了。
但是用更少的数据来表示缩略图确实是可行的,这里就需要介绍 BlurHash 这个工具了。BlurHash 的算法主要是对原图进行了离散余弦变换,提取出最多 10x10=100 个系数,以 0-18 的范围(也就是大约 4.25 bit 的精度,不过对于缩略图已经够用了)表示颜色,按照一些紧凑的规则编码成二进制数据,再使用特别选择的字母表进行 Base83 编码,保证在 URL 和 JSON 等地方不需要额外转义就可以直接传输。详细的编码规则可以参见这里。
在系数数量为 6x4 的情况下从上面的缩略图得到的 BlurHash 只有 52 B。即使将 10x10 个系数用满,由此得到的最长的 BlurHash 也只有 166 B,这个数据量比 Facebook 的极端优化还要小。
需要展示缩略图的时候,只要使用解码库就可以直接将 BlurHash 解码成任意尺寸的位图。系数越少得到的图片就越模糊(或者说是平滑?),但是就算是 10x10 也比前面使用的添加了高斯模糊的低分辨率 JPEG 还要模糊。也许有人会因为想要在模糊的缩略图中多展示一些图片细节而仍然选择低分辨率 JPEG,不过这就是个人喜好不同了 (´▽`)ノ♪
2023-04-30 更新:
以在前端开发领域目前被广泛使用的、跑得最快的 JS 打包器 esbuild 为代表作的 evanw 最近推出了 ThumbHash,原理和使用方式与 BlurHash 非常类似。根据官网上的对比,在相同数据量的情况下从 ThumbHash 解码的图像比 BlurHash 有更多的细节,颜色也更接近原图。
ThumbHash 不像 BlurHash 一样允许通过调节系数数量来控制数据量和解码图像的质量,不过如果本来就不想纠结这个的话其实并不算什么缺点。
在搜索的过程中,我另外还发现了一个名为 Primitive 的比较有意思的工具,可以用数百个椭圆、矩形、三角形等简单的图形对图片进行拟合,还支持以 SVG 的矢量格式输出。
这个工具是用 Go 写的,不过遗憾的是我没有 Go 语言环境,因此就没办法亲自体验了,作者提供的预编译版也只有 macOS 版。这种图片也是可以当缩略图使用的,虽然压到 2 KB 以内大概是做不到了……不过这种画风本身还是挺有趣的 (っ’ω’)っ
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。不允许内容农场类网站、CSDN 用户和微信公众号转载。
本文作者:✨小透明・宸✨
本文链接:https://akarin.dev/2021/11/04/progressive-image-loading/