存放在github.com上的源代码链接 Go语言处理Windows系统的图标ICO文件(上) Go语言处理Windows系统的图标ICO文件(下)
在上一篇文章中,我们了解了ico文件的结构,在这一篇文章中,我们首先来看看如何将多icon资源的ico文件中的图标图像提取出来。 从我选中的部分,我们已经知道了,该ico文件有25个ico图像(png和bmp),从开头的22个字节后,每16字节为一个ico文件的header,直到第一个header中的偏移量为止(19-22字节所描述的偏移量)。
那么将我们的理解转换为代码如下: 1、通过22个字节的header中,最后4个字节,我们获取icon图标头结构偏移量0x960100,即:‘406bytes offset’。 2、我们获取的0-406的内容就是我们第一个ico文件中所有icon图标的structure header。 3、根据icon数量来循环处理22个字节后的其他icon文件的header 声明与定义一个读取ico文件的函数:
func LoadIconFile(rd io.Reader) (icon *WinIcon, err error)如果我们完成了读取,则返回一个*WinIcon对象的指针,如果失败则返回错误对象。 那么我们的错误应该有哪些呢? 1、非法的文件(非法的ico文件) a、文件的长度连22个字节都没(说明根本就不是一个ico文件,只是扩展名是.ico) b、文件的长度的确超过了22字节,但是这22字节的内容却和ico的header结构不匹配 c、可能传入的读取像不是一个*os.File,亦或则不是一个文件呢…可能需要做些基本判断 d、在读取数据中,会不会出现越界的情况呢? 先暂时想到这里。 这里我定义了一些错误信息对象:
// 定义变量 var ( // 错误信息 ErrIcoInvalid = errors.New("ico: Invalid icon file") // 无效的ico文件 ErrIcoReaders = errors.New("ico: Reader type is not os.File pointer") // LoadIconFile的io.Reader参数不是文件指针 ErrIcoFileType = errors.New("ico: Reader is directory, not file") // io.Reader的文件指针是目录,不是文件 ErrIconsIndex = errors.New("ico: Slice out of bounds") // 读取ico文件时,可能出现的切片越界错误 )定义两个常量:
// 定义常量 const ( fileHeaderSize = 6 // 文件头的大小 headerSize = 16 // icon图标的头结构大小 )所以我们还需要一个函数来判断是否是ico文件:
func getIconHeader(b []byte) (wih *winIconHeader, err error)在这个函数中,我们判断文件合法性,如果非法,则返回err,如果合法,则提取icon的结构。 如果是单icon图标的ico文件,那么结构中的文件数量则为1,如果是多icon图标的ico文件,则文件数量为n,我们可以根据这个n来循环。或则为1则直接读取icon图标数据。
文件头只有6个字节,只是做一个简单判断,即3个部分,保留字段、是否是ico的marker,icon图标的数量getIconHeader的代码实现:
if len(b) != fileHeaderSize { return nil, ErrIcoInvalid } reserved := binary.LittleEndian.Uint16(b[0:2]) filetype := binary.LittleEndian.Uint16(b[2:4]) imagecount := binary.LittleEndian.Uint16(b[4:6]) if reserved != 0 || filetype != 1 || imagecount == 0 { return nil, ErrIcoInvalid } header := &winIconFileHeader{ ReservedA: reserved, FileType: filetype, ImageCount: imagecount, } return header, nil我们获取到了文件的头结构后,也就获得了icon图标的数量,根据数量我们可以控制循环的次数。 这里是部分LoadIconFile函数的代码:
// 创建一个 winIconStruct 数组切片 icos := make([]winIconStruct, int(icoHeader.ImageCount)) // 根据文件头中表示的icon图标文件的数量进行循环 structOffset := fileHeaderSize // 这里的icoHeader就是文件头,ImageCount就是我们获取的ico图标数量 for i := 0; i < int(icoHeader.ImageCount); i++ { // data 是怎个文件的[]byte数据 wis := getIconStruct(data, structOffset, headerSize) structOffset += headerSize icos[i] = *wis } // 这个循环的意义在于,我们将ico图标的头结构全部拿到,拿到这个就可以 // 根据ico图标的头结构信息来获取偏移量,从而获取图标图像的数据 // 创建 WinIcon 对象 ico = &WinIcon{ fileHeader: icoHeader, icos: icos, data: data, } return ico, nil获取所有数据:
func getFileAll(rd *bufio.Reader, size int64) (fb []byte, err error)实现:
data := make([]byte, size) // 丢弃1-6字节内容(文件头) // if _, err := rd.Discard(fileHeaderSize); err != nil { // return nil, err // } // size = size - fileHeaderSize for i := int64(0); i < size; i++ { b, err := rd.ReadByte() if err != nil { return nil, err } data[i] = b } return data, nil当我们有了文件的所有数据,以及icon图标的所有头结构后,我们就可以获取图标数据了:
func (wi *WinIcon) GetImageData(index int) (d []byte, err error) func (wi *WinIcon) getImageData(data []byte, offset, datasize int) []bytegetImageData是包内的private:成员函数,GetImageData是包外public:成员函数
从参数上看GetImageData仅需要传递索引即可,getImageData需要传递的内容是所有数据data,以及ico文件在所有数据中的偏移量,以及数据大小(length长度),最后返回ico的图像数据:[]byte类型。
getImageData代码实现:
var d = make([]byte, datasize) // 不建议 data[offset:length] 采用复制数据会相对安全些 for i, j := offset, 0; i < datasize+offset; i++ { d[j] = data[i] j++ } return dGetImageData 代码实现:
if index >= wi.getIconsHeaderCount() || index < 0 { return nil, ErrIconsIndex } wis := wi.icos[index] db := wi.getImageData(wi.data, int(wis.ImageOffset), int(wis.ImageDataSize)) return db, nilwi.getIconsHeaderCount获取我们已经的得到的icon图标头结构的数量。
基本上而言,以上代码就是读取数据的主要内容。完整代码请见页面中的github.com的链接。
好了,当我们实现了ico文件的读取/解析功能后,我们就可以实现提取数据的功能了。 函数签名:
func (wis winIconStruct) generateFileNameFormat(prefix string, width, height, bit int) string func (wis winIconStruct) IconToFile(path string, data []byte) error func (wi *WinIcon) ExtractIconToFile(filePrefix, filePath string) error func (wi *WinIcon) IconToFile(filePath string, index int) error这里顺便提一下,关于WinIcon、winIconStruct,在之前的结构体定义的时候,WinIcon 是首字母大写的,意思就是它是包级的,包外可以访问,而winIconStruct是包内的,包外是隐藏,我们包内实现,包内使用。 所以generateFileNameFormat、IconToFile 并不是提供给调用ico包的角色使用的。
这里就贴一下(wi *WinIcon) ExtractIconToFile ,我们可以使用该函数将ico文件中所有的icon图标全部提取出来,并保存到磁盘,而(wis winIconStruct) generateFileNameFormat 就是自动生成文件名前缀的函数。生成后效果如下: favicon_all.ico 这个文件就是多icon图标结构的ico文件,使用了30天试用版的某IconWorkshop软件生成(我不是美术,所以花钱买,貌似辱没了Gopher呀,而且买了,我就没必要写这个教程了?,我的win10三套都是正版,office正版,Xmind正版…,基本上我很少用盗版,嗯…不好意思,跑题了??),2-4个ico文件,分辨率为0x0,是因为图标的width和height数据段使用1byte,前面说了,1byte的最大正整数为255,而256这个数字则会被储存为0x00,高位丢失。所以在ico文件中如果icon头结构中的w,h字段值为0,那么如果没有出错的话,分辨率则为256x256pixel。
func (wi *WinIcon) ExtractIconToFile(filePrefix, filePath string) errorExtractIconToFile 的实现:
for _, v := range wi.icos { fileName := v.generateFileNameFormat(filePrefix, int(v.Width), int(v.Height), int(v.BitsPerPixel)) fp := filepath.Join(filePath, fileName) d := wi.getImageData(wi.data, int(v.ImageOffset), int(v.ImageDataSize)) if err := v.IconToFile(fp, d); err != nil { return err } } return nil这里我贴出package的测试ico_test.go
package ico import ( "log" "os" "path/filepath" "testing" ) func TestLoadIconFile(t *testing.T) { var fs *os.File defer func() { if fs != nil { fs.Close() } if err := recover(); err != nil { log.SetFlags(log.Ldate | log.Ltime | log.Llongfile) log.Printf("LoadIconFile() = Error:%v\r\n", err) os.Exit(1) } }() path := "../testico/" file := "favicon_all.ico" filePath := filepath.Join(path, file) fs, err := os.Open(filePath) if err != nil { panic(err) } wi, err := LoadIconFile(fs) if err != nil { panic(err) } // if err := wi.ExtractIconToFile("test", "../testdata/", 0); err != nil { // panic(err) // } // for _, v := range wi.icos { // fmt.Printf("image width:%v, height:%v\r\n", v.Width, v.Height) // fmt.Printf("image offset:%v, datesize:%v\r\n", v.ImageOffset, v.ImageDataSize) // d := wi.getImageData(wi.data, int(v.ImageOffset), int(v.ImageDataSize)) // fn := fmt.Sprintf("../testico/%vx%v@%v.ico", v.Width, v.Height, v.BitsPerPixel) // err := ioutil.WriteFile(fn, d, 0) // if err != nil { // fmt.Println(err) // } // } if err := wi.ExtractIconToFile("test", "../testico/"); err != nil { panic(err) } }我们使用go语言封装的时候,没必要写main package和main函数来跑代码,golang给我们自带了UT,善加利用,即可提高效率和质量。
提取后的png文件头部: 提取后的dib(bitmap)文件头部:
我们需要实现对png或bmp的数据检测函数。 以下是具有像素格式RGB24的2×2像素,24位位图(Windows DIB标题BITMAPINFOHEADER)的示例: 以下是具有像素格式ARGB32的alpha通道(Windows DIB标题BITMAPV4HEADER)中具有不透明度值的4×2像素,32位位图的示例: PNG文件头结构: 关于提取的*.ico文件数据,实际上还需要进一步处理,bitmap需要写一个bmp头,因为ico文件中的bitmap icon图标是没有BMP header,只有DIB header的。而png没有这个问题,我们需要识别一下后,bmp的保存为 *.bmp,png的保存为*.png。有了提取,我们还需要实现根据命令行参数将多个bmp或png文件打包为ico文件结构并输出ico文件的功能。而这些内容,我们将在下一集教程中讲解。
下面是读取ico文件,实现了全部提取及单独提取及单独提取并保存为单icon图标的ico文件的功能: 打包功能还没有写,下一章教程实现。
/* _____ __ __ _ __ ╱ ____| | ╲/ | | |/ / | | __ ___ | ╲ / | __ _ _ __| ' / | | |_ |/ _ ╲| |╲ /| |/ _` | '__| < | |__| | __/| | | ( _| | | | . ╲ ╲_____|╲___ |_| |_|╲__,_ |_| |_|╲_╲ 可爱飞行猪❤: golang83@outlook.com ??? Author Name: GeMarK.VK.Chow奥迪哥 ??? Creaet Time: 2019/05/25 - 07:51:34 ProgramFile: ico.go Description: Windows系统的ico文件工具包 */ package ico import ( "bufio" "bytes" "encoding/binary" "errors" "fmt" "io" "io/ioutil" "os" "path/filepath" "runtime" ) // 定义常量 const ( typeUKN = iota // unknow type typeBMP // bmp ico typePNG // png ico fileHeaderSize = 6 // 文件头的大小 headerSize = 16 // icon图标的头结构大小 bitmapHeaderSize = 14 // 位图文件头 dibHeaderSize = 40 // dib结构头 ) // 定义变量 var ( // 错误信息 ErrIcoInvalid = errors.New("ico: Invalid icon file") // 无效的ico文件 ErrIcoReaders = errors.New("ico: Reader type is not os.File pointer") // LoadIconFile的io.Reader参数不是文件指针 ErrIcoFileType = errors.New("ico: Reader is directory, not file") // io.Reader的文件指针是目录,不是文件 ErrIconsIndex = errors.New("ico: Slice out of bounds") // 读取ico文件时,可能出现的切片越界错误 ) // 类型定义 // 定义icon图标数据的存放 type ( ICONTYPE int WinIconData [][]byte WinIconStruct []winIconStruct ) // 定义 Windows 系统的 Ico 文件结构 type WinIcon struct { fileHeader *winIconFileHeader // 文件头 icos WinIconStruct // icon 头结构 icod WinIconData // 单独的icon图标的数据 data []byte // 所有ico文件数据 } type winIconFileHeader struct { ReservedA uint16 // 保留字段,始终为 '0x0000' FileType uint16 // 图像类型:'0x0100' 为 ico,'0x0200' 为 cur ImageCount uint16 // 图像数量:至少为 '0x0100' 即 1个图标 } type winIconStruct struct { Width uint8 // 图像宽度 Height uint8 // 图像高度 Palette uint8 // 调色板颜色数,不使用调色版为 '0x00' ReservedB uint8 // 保留字段,始终为 '0x00' ColorPlanes uint16 // 在ico中,指定颜色平面,'0x0000' 或则 '0x0100' BitsPerPixel uint16 // 在ico中,指定每像素的位数,如:'0x2000' 32bit ImageDataSize uint32 // 图像数据的大小,单位字节 ImageOffset uint32 // 图像数据的偏移量 } type dibHeader struct { dibSize uint32 // 4bytes bitmapWidth uint32 // 4bytes bitmapHeight uint32 // 4bytes colorPlanes uint16 // 2bytes BitsPerPixel uint16 // 2bytes markerBI_RGB uint32 // 4bytes originalSize uint32 // 4bytes printResH uint32 // 4bytes printResV uint32 // 4bytes Palette uint32 // 4bytes importantColor uint32 // 4bytes } type bitmapHeader struct { bitmapID uint16 fileSize uint32 unusedA uint16 unusedB uint16 bitmapDataOffset uint32 } // createBitmapHeader 创建位图文件头结构 func createBitmapHeader(datasize int) *bitmapHeader { return &bitmapHeader{ bitmapID: binary.LittleEndian.Uint16([]byte{0x42, 0x4d}), fileSize: uint32(datasize + bitmapHeaderSize), unusedA: 0, unusedB: 0, bitmapDataOffset: uint32(bitmapHeaderSize + dibHeaderSize), } } // GetIconType 获取icon的数据类型 func GetIconType(d []byte) ICONTYPE { if checkDIBHeader(d) { return typeBMP } if checkPNGHeader(d) { return typePNG } else { return typeUKN } } // checkPNGHeader 检测是否是png ico数据 func checkPNGHeader(d []byte) bool { if len(d) < 8 { return false } if bytes.Compare(d[0:8], []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}) != 0 { return false } return true } // headerToBytes 将bitmapHeader位图头结构转换为字节切片 func (bmh *bitmapHeader) headerToBytes() []byte { d := make([]byte, bitmapHeaderSize) binary.LittleEndian.PutUint16(d[0:2], bmh.bitmapID) binary.LittleEndian.PutUint32(d[2:6], bmh.fileSize) binary.LittleEndian.PutUint16(d[6:8], bmh.unusedA) binary.LittleEndian.PutUint16(d[8:10], bmh.unusedB) binary.LittleEndian.PutUint32(d[10:14], bmh.bitmapDataOffset) return d } // JoinHeader 将bitmapfileheader链接到含有dib头的位图数据前 func (bmh *bitmapHeader) JoinHeader(d []byte) []byte { h := bmh.headerToBytes() j := [][]byte{h, d} return bytes.Join(j, nil) } // checkDIBHeader 检测是否是bmp ico数据 func checkDIBHeader(d []byte) bool { if len(d) < 40 { return false } a := d[0:4] b := []byte{0x28, 0, 0, 0} if bytes.Compare(a, b) != 0 { return false } return true } // 将ico文件的数据载入到内存 func LoadIconFile(rd io.Reader) (icon *WinIcon, err error) { // 类型断言 v, t := rd.(*os.File) if !t { return nil, ErrIcoReaders } // 声明与定义变量 var ( fileSize int64 ico *WinIcon ) // 获取文件信息及判断是否是文件,而不是目录 fi, err := v.Stat() if err != nil { return nil, err } if fi.IsDir() { return nil, ErrIcoFileType } fileSize = fi.Size() // 创建缓冲IO的Reader对象窥视6个字节的文件头 reader := bufio.NewReader(rd) p, err := reader.Peek(fileHeaderSize) if err != nil { return nil, err } // 检测文件头及获取头结构 icoHeader, err := getIconFileHeader(p) if err != nil { return nil, err } // 获取ico文件的所有数据 data, err := getFileAll(reader, fileSize) if err != nil { return nil, err } // 创建一个 winIconStruct 数组切片 icos := make(WinIconStruct, int(icoHeader.ImageCount)) icod := make(WinIconData, int(icoHeader.ImageCount)) // 根据文件头中表示的icon图标文件的数量进行循环 structOffset := fileHeaderSize for i := 0; i < int(icoHeader.ImageCount); i++ { wis := getIconStruct(data, structOffset, headerSize) icodata := wis.getImageData(data, wis.getIconOffset(), wis.getIconLength()) structOffset += headerSize icos[i] = *wis icod[i] = icodata } // 创建 WinIcon 对象 ico = &WinIcon{ fileHeader: icoHeader, icos: icos, icod: icod, data: data, } return ico, nil } // getFileAll 获取ico文件所有数据(不包括文件头的6个字节) // rd *bufio.Reader: 对象 // size int64: 文件大小(我们需要读取的总数量) // fb []byte: 文件的所有数据,如果成功读取的话 // err error: 如果读取出现错误,返回错误 func getFileAll(rd *bufio.Reader, size int64) (fb []byte, err error) { data := make([]byte, size) for i := int64(0); i < size; i++ { b, err := rd.ReadByte() if err != nil { return nil, err } data[i] = b } return data, nil } // getIconFileHeader 获取文件头结构 // b []byte: 读取的数据来自这个字节切片 // wih *winIconFileHeader: 如果获取成功返回 winIconFileHeader对象指针 // err error: 如果读取发生错误,则返回错误信息 func getIconFileHeader(b []byte) (wih *winIconFileHeader, err error) { if len(b) != fileHeaderSize { return nil, ErrIcoInvalid } reserved := binary.LittleEndian.Uint16(b[0:2]) filetype := binary.LittleEndian.Uint16(b[2:4]) imagecount := binary.LittleEndian.Uint16(b[4:6]) if reserved != 0 || filetype != 1 || imagecount == 0 { return nil, ErrIcoInvalid } header := &winIconFileHeader{ ReservedA: reserved, FileType: filetype, ImageCount: imagecount, } return header, nil } // getIconStruct 根据 offset, length 来获取icon图标结构 // b []byte: 文件数据的字节切片 // offset int: 偏移量 // length int: 数据长度 func getIconStruct(b []byte, offset, length int) (wis *winIconStruct) { var std []byte //std = b[offset:length] std = make([]byte, headerSize) j := 0 for i := offset; i < length+offset; i++ { std[j] = b[i] j++ } is := &winIconStruct{ Width: std[0], Height: std[1], Palette: std[2], ReservedB: std[3], ColorPlanes: binary.LittleEndian.Uint16(std[4:6]), BitsPerPixel: binary.LittleEndian.Uint16(std[6:8]), ImageDataSize: binary.LittleEndian.Uint32(std[8:12]), ImageOffset: binary.LittleEndian.Uint32(std[12:]), } return is } // getImageData 根据 offset, length 参数获取图标图像数据 // data []byte: 图像数据的字节切片 // offset int: 图像数据的偏移量 // length int: 图像数据的长度 // return []byte: 返回获取的数据字节切片 func (wi *WinIcon) getImageData(data []byte, offset, datasize int) []byte { var d = make([]byte, datasize) //data[offset:length] for i, j := offset, 0; i < datasize+offset; i++ { d[j] = data[i] j++ } return d } func (wis winIconStruct) getImageData(data []byte, offset, ds int) []byte { var d = make([]byte, ds) for i, j := offset, 0; i < ds+offset; i++ { d[j] = data[i] j++ } return d } // ExtractIconToFile 提取 ico 数据到文件 // filePrefix string: 为前缀,如果传如空字符串,则没有前缀,使用数字和分辨率作为文件名 // filePath string: 提取的数据写入的路径,空字符串则将文件保存到当前目录 // 舍弃:--count int: 提取文件的数量,0: 为所有,> 0 则根据已保存的map对象来提取对应数量内容,指定数量超出实际数量则全部提取-- // 该函数不检测路径的有效性,使用者自己把控,如果路径有问题,会返回error对象 func (wi *WinIcon) ExtractIconToFile(filePrefix, filePath string) error { var ext string for i, v := range wi.icos { w := v.getIconWidth() h := v.getIconHeight() b := v.getIconBitsPerPixel() d, _ := wi.GetImageData(i) if GetIconType(d) == typeBMP { ext = "bmp" } else { ext = "png" if w == 0 && h == 0 { w = 256 h = 256 } } fn := v.generateFileNameFormat(filePrefix, ext, w, h, b) if err := wi.IconToFile(filePath, fn, i); err != nil { return err } } return nil } // GetImageData 获取ico图标的图像数据 // index int: 下标索引,0序 // 如果越界或读取数据错误,返回 error 对象 func (wi *WinIcon) GetImageData(index int) (d []byte, err error) { if index >= wi.getIconsHeaderCount() || index < 0 { return nil, ErrIconsIndex } wis := wi.icos[index] offset := wis.getIconOffset() datasize := wis.getIconLength() data := wi.getImageData(wi.data, offset, datasize) return data, nil } // IconToFile 将图标写入文件 // path string: 文件写入的路径 // name string: 文件名 // error 如果写入发生错误,则返回错误信息 // IconToFile 并不会检测路径是否有效 func (wi *WinIcon) IconToFile(path, name string, index int) error { if index >= wi.getIconsHeaderCount() || index < 0 { return ErrIconsIndex } wis := wi.icos[index] p := filepath.Join(path, name) d, e := wi.GetImageData(index) if e != nil { return e } // 处理bitmap头结构 if GetIconType(d) == typeBMP { w := wis.getIconWidth() h := wis.getIconHeight() b := wis.getIconBitsPerPixel() s := len(d) - dibHeaderSize dib := createDIBHeader(w, h, b, s, 0, 0) err := dib.EditDIBHeader(d) if err != nil { return err } bmh := createBitmapHeader(len(d)) d = bmh.JoinHeader(d) } if e := wis.IconToFile(p, d); e != nil { return e } else { return nil } } // IconToIcoFile 将ico文件中的指定icon图标数据写入ico文件 // path string: 路径(不检查合法性) // index int: icon图标的索引 // error: 如果发生错误返回error对象 func (wi *WinIcon) IconToIcoFile(path string, index int) error { if index < 0 || index >= len(wi.icos) { return ErrIconsIndex } d := wi.icod[index] wis := wi.icos[index] wis.ImageOffset = fileHeaderSize + headerSize wis.ImageDataSize = uint32(len(d)) d = wis.joinHeader(d) if e := ioutil.WriteFile(path, d, getPerm()); e != nil { return e } return nil } // getIconsHeaderCount 获取 icons 图标的结构数量-可能和头结构的ico数量不一致,只是可能 // 返回值为数量,类型 int func (wi *WinIcon) getIconsHeaderCount() int { return len(wi.icos) } // generateFileNameFormat 产生文件名 func (wis winIconStruct) generateFileNameFormat(prefix, ext string, width, height, bit int) string { return fmt.Sprintf("%s_icon%dx%d@