Flutter&Rust#04 | 图片编解码

763 阅读7分钟

Flutter&rust1.png


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是我们前进的灯塔。

所以编码的价值之一在于:

通过编码规则,减少 数据信息 中的冗余内容,达到减少体积的目的。


在程序中,可以读取封装信息的内容,再根据解码的规则,还原出原始的数据信息。这种编码就可以实现 无损压缩 的效果。

image.png


2. 图片的编解码

对于图片来说,编解码也是类似的。像素数据记录了 完整 的色彩信息,而像 pngjpegwebp 这种图片文件,就是通过对应的编码规则,生成的封装格式,也就是上面压缩版的数据信息,可以减少数据体积。

image.png

应用程序在读取封装格式后,底层会对封装格式进行解码,得到原始数据,进行渲染。编码之后,可以通过解码得到完整的数据信息,称之为 无损压缩;反之为 有损压缩


举个小例子,如下所示这张 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%

image.png


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 个字节:

image.png


3. 图片文件里到底存了啥

这里我将化繁为简,准备了一张只有一个红色像素(0xFF0000)的 24 位 png 图片,可以看出它占 90 字节。算数好的朋友可能会疑惑,一个像素点原始的色彩数据不应该是 24 位 = 3 字节吗? 怎么还给压缩大了?

image.png

我们可以同样将上面的 png 图片还原成原始数据,如下所示,这张 png 图片记录的原始数据确实只是占 3 字节:

image.png

通过 Bainary Viewer 可以看出原始数据记录的就是颜色的 rgb 值,对应的 16 进制数据是 FF0000 :

image.png

如果准备的是一张具有透明度的 PNG 图片,那它的位深将是 32 ,也就是一个像素点记录着 rgba 四个数值:

image.png


通过 Bainary Viewer 查看 png 文件可以看出,文件头部包含着编码的标识信息。这样软件在访问图片时可以了解它的编码方式,从而选用对应的解码器得到原始数据。png 文件的开头有 8 字节的固定位:

89 50 4E 47 0D 0A 1A 0A

image.png


JPEG 图片文件以 0xFFD8 开头,从一像素图片来看,JPEG 记录的编码信息要更多。其他格式的图片大家也可以自己去看看。

image.png


3. 图片的格式转换是在干什么

图片的格式转换,本质上就是将从封装格式中 解码 成原始的像素数据,再通过像素数据 编码 成指定的封装格式:

image.png

在 Rust 的 image 库中,有 save_with_format 方法,可以将解码后的数据,以指定的格式存储。其中参数 ImageFormat 就是该库支持的图片编码格式:

image.png

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 来得到对应的枚举对象:

image.png

这样我们就可以封装一个通用的图片格式转换函数 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 的滤色器、压缩比等。如果想要操纵更多参数,可以像上篇那样主动创建编码器来处理。

image.png


到这里,可能会有人不禁发问?为什么不能统一编码格式,这样就不用这么复杂的转换了。

你可以反问一下:为什么世界上会有这么多语言?为什么编程有这么多框架?为什么有这么多的浏览器、编辑器? 为什么要有这么多姓氏、学校、企业?不能统一吗,这样就没有复杂地抉择、学习、对比了。

因为世上没有什么万能药,单一就缺乏竞争,没了“物竞”,在“天择”的那刻就是群体的毁灭。物种的多样性,可以更好地适应不同的场景。时间会淘汰那些不合时宜的物种,而现在的,存在即合理。