news 2026/5/11 6:10:11

为pngme拓展对gif格式支持

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
为pngme拓展对gif格式支持

为pngme拓展对gif格式支持

  • 引言
  • GIF格式简读
  • GIF结构
  • 原始数据到结构数据
    • 读屏幕逻辑数据
    • 读图像数据
    • 读取扩展块
    • 读取子块链
  • 将文本写入应用扩展块
  • 结构数据到原始数据

引言

前序文章为pngme拓展加密功能与jpg格式支持-CSDN博客,其中所提到的东西,本文不会重复提及,除了我的演示项目地址Smart-Space/pngkey: 将信息文本或密文写入PNG、JPG或GIF文件。

GIF格式文件也是一种常见的图片,所以我决定为pngkey添加对gif格式的支持,这样,对于三大图片格式(png, jpg, gif)都能用了。本文使用的gif格式为89a


GIF格式简读

中文文章参考GIF图解与压缩详解-CSDN博客,细节部分本文不会提及。

不同于PNG与JPG的通用块结构,GIF包括:

  • 文件头
  • 逻辑屏幕标识符
  • 全局颜色列表
  • 图像块若干
  • 拓展块若干
  • 文件结尾

每个数据块的数据解释不尽相同,因此,需要构建多个块结构,同时实现各个块类型从原始数据到结构数据,以及其逆变换的方法。

另外,我们能写入的块为Comment ExtensionApplication Extension,但是仔细想想,注释信息不好说会不会影响其他程序解析gif,而且注释块根本就没有给我们填写块标识符的地方,或者说,其存储逻辑本身没有标识符的位置。而且,使用应用扩展块,可以在Application Identifier位置写入" pngkey "用来表示这个块被我们使用,同时将Application Authentication Code作为块识别码。因此,我们选择应用扩展块来进行数据存储。

GIF结构

/// GIF结构pubstructGif{chunks:Vec<Chunk>,}pubenumChunk{Header([u8;6]),LogicalScreenDescriptor(LogicalScreenDescriptor),GlobalColorTable(Vec<u8>),Image(ImageChunk),Extension(ExtensionChunk),Trailer,}pubstructLogicalScreenDescriptor{pubwidth:u16,pubheight:u16,pubpacked_fields:u8,pubbackground_color_index:u8,pubpixel_aspect_ratio:u8,}pubstructImageChunk{pubdescriptor:ImageDescriptor,publocal_color_table:Option<Vec<u8>>,pubimage_data:Vec<u8>,// 包含LZW压缩数据的子块链}pubstructImageDescriptor{publeft:u16,pubtop:u16,pubwidth:u16,pubheight:u16,pubpacked_fields:u8,}pubstructExtensionChunk{pubextension_type:u8,pubdata:Vec<u8>,// 包括块头长度与子块链}

原始数据到结构数据

开始前说一句,rust的std::io::Cursor好好玩😋。

其实大体上就是按照gif标准格式从头到尾解析一遍,注意是小端序列就行:

implTryFrom<&[u8]>forGif{typeError=Error;fntry_from(bytes:&[u8])->Result<Gif>{letmutcursor=std::io::Cursor::new(bytes);letmutchunks=Vec::new();letmutheader=[0u8;6];cursor.read_exact(&mutheader)?;chunks.push(Chunk::Header(header));// pngkey会事先判断gif头,因此这里不判断了letlsd=Self::read_logical_screen_descriptor(&mutcursor)?;lethas_gct=(lsd.packed_fields&0x80)!=0;letgct_size_factor=lsd.packed_fields&0x07;chunks.push(Chunk::LogicalScreenDescriptor(lsd));ifhas_gct{letsize=2u16.pow((gct_size_factor+1)asu32)asusize;letmutgct=vec![0u8;size*3];// RGB / 1 bytecursor.read_exact(&mutgct)?;chunks.push(Chunk::GlobalColorTable(gct));}// 读取所有数据直到Trailerloop{letmutblock_type=[0u8;1];cursor.read_exact(&mutblock_type)?;matchblock_type[0]{0x2c=>{// ','分割图像letimage=Self::read_image_chunk(&mutcursor)?;chunks.push(Chunk::Image(image));}0x21=>{// '!'拓展块letextension=Self::read_extension_chunk(&mutcursor)?;chunks.push(Chunk::Extension(extension));}0x3b=>{// 结尾chunks.push(Chunk::Trailer);break;}_=>{returnErr("Invalid GIF block type".into());}}}Ok(Gif{chunks})}}

读屏幕逻辑数据

fnread_logical_screen_descriptor<R:Read>(reader:&mutR)->Result<LogicalScreenDescriptor>{letmutbuf=[0u8;7];reader.read_exact(&mutbuf)?;Ok(LogicalScreenDescriptor{width:u16::from_le_bytes([buf[0],buf[1]]),height:u16::from_le_bytes([buf[2],buf[3]]),packed_fields:buf[4],background_color_index:buf[5],pixel_aspect_ratio:buf[6],})}

这没什么好说的,七个字节直接读就完事了,当packed_fields的最高位置位时表示有全局颜色列表GlobalColorTable

读图像数据

fnread_image_chunk<R:Read>(reader:&mutR)->Result<ImageChunk>{letmutbuf=[0u8;9];reader.read_exact(&mutbuf)?;letdescriptor=ImageDescriptor{left:u16::from_le_bytes([buf[0],buf[1]]),top:u16::from_le_bytes([buf[2],buf[3]]),width:u16::from_le_bytes([buf[4],buf[5]]),height:u16::from_le_bytes([buf[6],buf[7]]),packed_fields:buf[8],};// 检查局部调色板letlocal_color_table=if(descriptor.packed_fields&0x80)!=0{letsize=2u16.pow(((descriptor.packed_fields&0x07)+1)asu32)asusize;letmutlct=vec![0u8;size*3];reader.read_exact(&mutlct)?;Some(lct)}else{None};// 读取图像数据(LZW压缩数据的子块链)letmutlzw_min=[0u8;1];reader.read_exact(&mutlzw_min)?;letmutimage_data=vec![lzw_min[0]];letsub=Self::read_sub_blocks(reader)?;image_data.extend_from_slice(&sub);Ok(ImageChunk{descriptor,local_color_table,image_data,})}

前半部分跟全局颜色数据的解析基本一致。后半部分要注意,LZW编码长度本身占用一个字节,之后才是读子块链。

读取扩展块

fnread_extension_chunk<R:Read>(reader:&mutR)->Result<ExtensionChunk>{letmutext_type=[0u8;1];reader.read_exact(&mutext_type)?;letmutdata=Vec::new();letmutsub=Self::read_sub_blocks(reader)?;data.append(&mutsub);Ok(ExtensionChunk{extension_type:ext_type[0],data,})}

扩展块要说明的是,标识头长度(固定为11)、标识头、子块链都在data成员里。

读取子块链

子块链在gif标准格式里才是通用的,一字节长度信息+数据信息作为一个单元。

fnread_sub_blocks<R:Read>(reader:&mutR)->Result<Vec<u8>>{letmutdata=Vec::new();loop{letmutsize_byte=[0u8;1];reader.read_exact(&mutsize_byte)?;data.push(size_byte[0]);letsize=size_byte[0]asusize;ifsize==0{break;// 子块链结束}letmutblock_data=vec![0u8;size];reader.read_exact(&mutblock_data)?;data.extend_from_slice(&block_data);}Ok(data)}

将文本写入应用扩展块

由于每个子块最多255字节数据 + 1字节长度,因此,对于要写入的信息,需要截断填入:

// ...letmutremaining_data=data;while!remaining_data.is_empty(){letchunk_size=std::cmp::min(remaining_data.len(),255);ext_data.push(chunk_sizeasu8);ext_data.extend_from_slice(&remaining_data[..chunk_size]);remaining_data=&remaining_data[chunk_size..];}// ...

结构数据到原始数据

对于扩展块,先前的读取步骤将块头长度与数据和子块链全部读入了data成员,因此内部数据原样输出即可,外加上扩展标识符就行(图片的0x2c,应用的0x21)。

pubfnas_bytes(&self)->Result<Vec<u8>>{letmutbytes:Vec<u8>=Vec::new();forchunkin&self.chunks{matchchunk{Chunk::Header(header)=>{bytes.write_all(header)?;}Chunk::LogicalScreenDescriptor(lsd)=>{bytes.write_all(&lsd.width.to_le_bytes())?;bytes.write_all(&lsd.height.to_le_bytes())?;bytes.write_all(&[lsd.packed_fields])?;bytes.write_all(&[lsd.background_color_index])?;bytes.write_all(&[lsd.pixel_aspect_ratio])?;}Chunk::GlobalColorTable(gct)=>{bytes.write_all(gct)?;}Chunk::Image(image)=>{bytes.write_all(&[0x2c])?;bytes.write_all(&image.descriptor.left.to_le_bytes())?;bytes.write_all(&image.descriptor.top.to_le_bytes())?;bytes.write_all(&image.descriptor.width.to_le_bytes())?;bytes.write_all(&image.descriptor.height.to_le_bytes())?;bytes.write_all(&[image.descriptor.packed_fields])?;ifletSome(lct)=&image.local_color_table{bytes.write_all(lct)?;}bytes.write_all(&image.image_data)?;}Chunk::Extension(extension)=>{bytes.write_all(&[0x21])?;bytes.write_all(&[extension.extension_type])?;bytes.write_all(&extension.data)?;}Chunk::Trailer=>{bytes.write_all(&[0x3b])?;}}}Ok(bytes)}
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/10 9:22:13

互联网大厂Java求职面试实战:微服务、缓存与AI技术全栈问答解析

互联网大厂Java求职面试实战&#xff1a;微服务、缓存与AI技术全栈问答解析 本文以互联网大厂Java求职者谢飞机与严肃面试官的对话形式&#xff0c;真实还原面试场景&#xff0c;涵盖Java SE 8/11/17、Jakarta EE、Spring Boot、微服务架构、数据库ORM、缓存技术、消息队列、安…

作者头像 李华
网站建设 2026/5/11 2:58:09

Linux驱动probe函数全解析:以蜂鸣器驱动为例,吃透初始化流程与规范

Linux驱动probe函数全解析&#xff1a;以蜂鸣器驱动为例&#xff0c;吃透初始化流程与规范 probe函数是Linux platform驱动的“灵魂入口”——当内核完成驱动与设备树/平台设备的匹配后&#xff0c;会调用probe函数完成驱动的核心初始化。本文以蜂鸣器驱动的probe函数为例&…

作者头像 李华
网站建设 2026/5/10 3:04:00

AI人工智能如何正确入行(小白程序员必收藏)

到底做什么&#xff0c;才算真正入行AI&#xff1f; 这个话题我在之前的分享中多次提及&#xff0c;今天结合工业界实际场景再系统梳理一遍&#xff1a;在企业中直接落地AI技术的岗位&#xff0c;核心可分为三大类——算法类、工程类、数据类。 当下网络上&#xff0c;无论是…

作者头像 李华
网站建设 2026/5/6 13:49:22

深入浅出 CAS:从 CPU 指令到 Java 17 原子类实战

这篇文章会用 Java 17 的视角&#xff0c;把 CAS 从底层原理到实际落地系统地讲清楚。 目标只有一个&#xff1a;看完这一篇&#xff0c;你对 CAS 的理解不再停留在“有三个参数 V/E/N”这种记忆层面&#xff0c;而是能从 CPU 指令一路推演到 Java 代码&#xff0c;再对框架源…

作者头像 李华
网站建设 2026/5/4 14:26:11

Thinkphp和Laravel基于Web的课程设计选题管理系统

目录ThinkPHP与Laravel框架的课程设计选题管理系统摘要项目开发技术介绍PHP核心代码部分展示系统结论源码获取/同行可拿货,招校园代理ThinkPHP与Laravel框架的课程设计选题管理系统摘要 基于ThinkPHP的选题管理系统 ThinkPHP作为国内流行的PHP框架&#xff0c;适合快速开发中小…

作者头像 李华