- 《Flutter&Rust#01 | 突破能力瓶颈》
- 《Flutter&Rust#02 | 图片灰度 - 性能提升!》
- 《Flutter&Rust#03 | 图片格式转换 jpeg/webp 》
- 《Flutter&Rust#04 | 图片编解码》
1. 编解码
在介绍图片的编解码之前,先通过一个小例子来了解一下编解码的概念。如下是一段文字,它占据了 465
字节:
在这个世界上,每个人都有自己的梦想。梦想是生活的动力。没有梦想的人,就像没有目标的船只。每个人的梦想都是独特的,梦想可以是成功,梦想可以是财富,梦想可以是自由,梦想可以是幸福。梦想让我们前进,梦想让我们坚持不懈,梦想让我们在困难面前不轻易放弃。梦想是每个人生活的目标,梦想给我们带来动力,梦想是我们前进的灯塔。
其中包含了较多 梦想
、梦想是
、梦想让我们
、每个人
的冗余信息。对于这段文字所表达的信息来说,我们可以指定一个编码规则,比如:
将 “梦想” -> A
将 “梦想是” -> B
将 “梦想让我们” -> C
将 “每个人” -> D
在这个编码规则下,下面的文字记录着和上面同样的信息,但它仅占 308
字节,体积压缩到 66.23% 。
在这个世界上,D都有自己的A。A是生活的动力。没有A的人,就像没有目标的船只。D的A都是独特的,B成功,B财富,B自由,B幸福。C前进,C坚持不懈,C在困难面前不轻易放弃。A是D生活的目标,A给我们带来动力,A是我们前进的灯塔。
所以编码的价值之一在于:
通过编码规则,减少 数据信息 中的冗余内容,达到减少体积的目的。
在程序中,可以读取封装信息的内容,再根据解码的规则,还原出原始的数据信息。这种编码就可以实现 无损压缩 的效果。
2. 图片的编解码
对于图片来说,编解码也是类似的。像素数据记录了 完整 的色彩信息,而像 png
、jpeg
、webp
这种图片文件,就是通过对应的编码规则,生成的封装格式,也就是上面压缩版的数据信息,可以减少数据体积。
应用程序在读取封装格式后,底层会对封装格式进行解码,得到原始数据,进行渲染。编码之后,可以通过解码得到完整的数据信息,称之为 无损压缩;反之为 有损压缩。
举个小例子,如下所示这张 png 图片的宽高是 2480*1486
,位深 24 表示 一个原始像素点通过 24 位表示。由于 8 位占一个字节,即一个原始像素点数据占 3 个字节,也就是 r、 g、b 三个通道色彩数据。
这么算来,这张图片有 2480*1486 = 3,685,280
个像素点,所以完整的色彩信息应该占据 11,055,840
字节,而实际来看,这张 png 的大小是 3,557,693
字节。可以得出
通过
png
的编码处理,这张图片 将原始的色彩信息数据压缩到了原来的 32.18%
png 是一种 无损压缩 的封装格式。我们可以将一张 png 图片通过 解码 还原出完整的色彩信息。下面代码中,通过 rust 代码将制定的封装格式图片,解码 (decode) 成原始的色彩信息。然后将数据保存到指定文件:
pub fn to_raw(input_path: &str, output_path: &str) -> Result<(), image::ImageError>{
let img = ImageReader::open(input_path)?.decode()?;
std::fs::write(output_path, img.as_bytes())?;
Ok(())
}
对比原始数据的大小,可以看出它恰好是 11,055,840
个字节:
3. 图片文件里到底存了啥
这里我将化繁为简,准备了一张只有一个红色像素(0xFF0000)的 24 位 png 图片,可以看出它占 90 字节。算数好的朋友可能会疑惑,一个像素点原始的色彩数据不应该是 24 位 = 3 字节吗? 怎么还给压缩大了?
我们可以同样将上面的 png 图片还原成原始数据,如下所示,这张 png 图片记录的原始数据确实只是占 3 字节:
通过 Bainary Viewer 可以看出原始数据记录的就是颜色的 rgb 值,对应的 16 进制数据是 FF0000
:
如果准备的是一张具有透明度的 PNG 图片,那它的位深将是 32 ,也就是一个像素点记录着 rgba 四个数值:
通过 Bainary Viewer 查看 png 文件可以看出,文件头部包含着编码的标识信息。这样软件在访问图片时可以了解它的编码方式,从而选用对应的解码器得到原始数据。png 文件的开头有 8 字节的固定位:
89 50 4E 47 0D 0A 1A 0A
JPEG 图片文件以 0xFFD8
开头,从一像素图片来看,JPEG 记录的编码信息要更多。其他格式的图片大家也可以自己去看看。
3. 图片的格式转换是在干什么
图片的格式转换,本质上就是将从封装格式中 解码 成原始的像素数据,再通过像素数据 编码 成指定的封装格式:
在 Rust 的 image 库中,有 save_with_format 方法,可以将解码后的数据,以指定的格式存储。其中参数 ImageFormat
就是该库支持的图片编码格式:
pub enum ImageFormat {
/// An Image in PNG Format
Png,
/// An Image in JPEG Format
Jpeg,
/// An Image in GIF Format
Gif,
/// An Image in WEBP Format
WebP,
/// An Image in general PNM Format
Pnm,
/// An Image in TIFF Format
Tiff,
/// An Image in TGA Format
Tga,
/// An Image in DDS Format
Dds,
/// An Image in BMP Format
Bmp,
/// An Image in ICO Format
Ico,
/// An Image in Radiance HDR Format
Hdr,
/// An Image in OpenEXR Format
OpenExr,
/// An Image in farbfeld Format
Farbfeld,
/// An Image in AVIF Format
Avif,
/// An Image in QOI Format
Qoi,
}
另外,ImageFormat 可以通过 from_extension 来得到对应的枚举对象:
这样我们就可以封装一个通用的图片格式转换函数 convert_image_format
。通过桥接让 Flutter 调用它即可,
pub fn convert_image_format(input_path: &str, output_path: &str) -> Result<(), image::ImageError> {
let img = image::open(input_path)?;
let output_extension = Path::new(output_path)
.extension()
.and_then(std::ffi::OsStr::to_str)
.unwrap_or("png");
let out_format = ImageFormat::from_extension(output_extension).unwrap();
img.save_with_format(output_path, out_format)?;
Ok(())
}
通过源码可以看出,image 库的底层转换逻辑是通过 format 枚举,得到对应格式的编码器进行处理,通过编码器将解码后的颜色数据重新编码成对应的格式。这个方法虽然便捷,但它无法更细致的操作编码器的配置,比如 JPEG 的编码质量、PNG 的滤色器、压缩比等。如果想要操纵更多参数,可以像上篇那样主动创建编码器来处理。
到这里,可能会有人不禁发问?为什么不能统一编码格式,这样就不用这么复杂的转换了。
你可以反问一下:为什么世界上会有这么多语言?为什么编程有这么多框架?为什么有这么多的浏览器、编辑器? 为什么要有这么多姓氏、学校、企业?不能统一吗,这样就没有复杂地抉择、学习、对比了。
因为世上没有什么万能药,单一就缺乏竞争,没了“物竞”,在“天择”的那刻就是群体的毁灭。物种的多样性,可以更好地适应不同的场景。时间会淘汰那些不合时宜的物种,而现在的,存在即合理。