1、初始化go.mod
go mod init github.com/xumeng03/images
2、编写包内容
这里只是一个简单的压缩jpg/jpeg图片例子,代码参考 https://github.com/disintegration/imaging
2.1、fs.go
package imagesimport ("image""io""os""path""strings"
)type FileSystem interface {Create(string) (io.WriteCloser, error)Open(string) (io.ReadCloser, error)
}type LocalFileSystem struct{}func (fs LocalFileSystem) Create(name string) (io.WriteCloser, error) {return os.Create(name)
}func (fs LocalFileSystem) Open(name string) (io.ReadCloser, error) {return os.Open(name)
}var fs FileSystem = LocalFileSystem{}func Open(filename string) (image.Image, error) {file, err := fs.Open(filename)if err != nil {return nil, err}defer file.Close()return Decode(file)
}func Close(img image.Image, filename string, quality int) error {file, err := fs.Create(filename)if err != nil {return err}ext := path.Ext(filename)err = Encode(file, img, strings.ReplaceAll(ext, ".", ""), quality)if err != nil {return err}err = file.Close()return err
}
2.2、image.go
package imagesimport ("fmt""image""image/jpeg"_ "image/jpeg"_ "image/png""io"
)func Decode(reader io.Reader) (image.Image, error) {// 数据写入 PipeWriter 对象后,可以通过相应的 PipeReader 对象进行读取;pr, pw := io.Pipe()// 创建了一个新的 io.Reader 对象,这个对象能够将从其读取的数据同时写入到另一个 io.Writer 中(如同包装类)reader = io.TeeReader(reader, pw)done := make(chan struct{})var orient orientationgo func() {defer close(done)orient = readOrientation(pr)io.Copy(io.Discard, pr)}()img, _, err := image.Decode(reader)pw.Close()<-donefmt.Println(orient)if err != nil {return nil, err}return img, nil
}func Encode(w io.Writer, img image.Image, t string, quality int) error {switch t {case "jpg":fallthroughcase "jpeg":if nrgba, ok := img.(*image.NRGBA); ok && nrgba.Opaque() {rgba := &image.RGBA{Pix: nrgba.Pix,Stride: nrgba.Stride,Rect: nrgba.Rect,}return jpeg.Encode(w, rgba, &jpeg.Options{Quality: quality})}return jpeg.Encode(w, img, &jpeg.Options{Quality: quality})default:println("type error!")return nil}
}
2.3、exif.go
package imagesimport ("encoding/binary""io"
)type orientation intconst (// 方向未指定orientationUnspecified = 0// 正常方向orientationNormal = 1// 需水平翻转orientationFlipH = 2// 需旋转180度orientationRotate180 = 3// 需垂直翻转orientationFlipV = 4// 需对角线翻转(左上到右下)orientationTranspose = 5// 需逆时针旋转270度orientationRotate270 = 6// 需对角线翻转(右上到左下)orientationTransverse = 7// 需顺时针旋转90度orientationRotate90 = 8
)const (// Start Of Image:表示 JPEG 图片流的起始markerSOI = 0xffd8// Application Segment 1:表示 APP1 区块,EXIF 信息通常存储在 APP1 区块内markerAPP1 = 0xffe1// Exif Header:表示 APP1 区块确实包含了 EXIF 信息(紧跟在 APP1 区块标识后),且后面通常跟着两个填充字节exifHeader = 0x45786966// Big Endian byte order mark:如果 EXIF 段使用大端字节序,那么其字节序标记为 'MM' (0x4D4D),即(高位字节排在前)byteOrderBE = 0x4d4d// Little Endian byte order mark:如果 EXIF 段使用小端字节序,那么其字节序标记为 'II' (0x4949),即(低位字节排在前)byteOrderLE = 0x4949// Orientation Tag:表示图像的方向orientationTag = 0x0112
)func readOrientation(reader io.Reader) orientation {// 检查 JPEG 开始标记(PNG 和 GIF 等格式不是传统意义上的摄影,图像元数据一般不包括拍摄方向信息。处理这些图像文件时,通常没有必要读取或调整图像方向)var soi uint16if binary.Read(reader, binary.BigEndian, &soi) != nil {return orientationUnspecified}if soi != markerSOI {return orientationUnspecified}for {var marker, size uint16if err := binary.Read(reader, binary.BigEndian, &marker); err != nil {return orientationUnspecified}if err := binary.Read(reader, binary.BigEndian, &size); err != nil {return orientationUnspecified}// 检查是否是有效的 JPEG 标记if marker>>8 != 0xff {return orientationUnspecified}// 检查是否为 APP1 标记if marker == markerAPP1 {break}// 对于任何 JPEG 数据块,其报告的大小应至少为2字节if size < 2 {return orientationUnspecified}// 这里的减2表示减去size本身占用的2字节(size表示的是从size开始这个段还有几个字节)if _, err := io.CopyN(io.Discard, reader, int64(size-2)); err != nil {return orientationUnspecified}}// 检查 exifHeader 标记var header uint32if err := binary.Read(reader, binary.BigEndian, &header); err != nil {return orientationUnspecified}if header != exifHeader {return orientationUnspecified}if _, err := io.CopyN(io.Discard, reader, 2); err != nil {return orientationUnspecified}// 从文件中读取的字节序标识var byteOrderTag uint16var byteOrder binary.ByteOrderif err := binary.Read(reader, binary.BigEndian, &byteOrderTag); err != nil {return orientationUnspecified}switch byteOrderTag {case byteOrderBE:byteOrder = binary.BigEndiancase byteOrderLE:byteOrder = binary.LittleEndiandefault:return orientationUnspecified}if _, err := io.CopyN(io.Discard, reader, 2); err != nil {return orientationUnspecified}// 跳过 exif 段var offset uint32if err := binary.Read(reader, binary.BigEndian, &offset); err != nil {return orientationUnspecified}if offset < 8 {// 在 TIFF 格式中,如果 offset 小于 8(byteOrderTag、填充字节、offset字节),那么它指向的位置是不合逻辑的,表明可能是一个损坏或非法格式的文件。return orientationUnspecified}if _, err := io.CopyN(io.Discard, reader, int64(offset-8)); err != nil {return orientationUnspecified}// 获取标签数var numTags uint16if err := binary.Read(reader, byteOrder, &numTags); err != nil {return orientationUnspecified}for i := 0; i < int(numTags); i++ {var tag uint16if err := binary.Read(reader, binary.BigEndian, &tag); err != nil {return orientationUnspecified}if tag != orientationTag {// 10 = 2(数据类型)+ 4(计数)+ 4(值或值偏移量)if _, err := io.CopyN(io.Discard, reader, 10); err != nil {return orientationUnspecified}continue}// 跳过2字节(数据类型)+ 4字节(计数)if _, err := io.CopyN(io.Discard, reader, 6); err != nil {return orientationUnspecified}// 读取方向值(在 TIFF 中,实际的方向值可以直接存放在“值或值偏移量”的位置,并且仅占用前两字节,剩余的两字节则不会包含任何重要信息)var direction uint16if err := binary.Read(reader, binary.BigEndian, &direction); err != nil {return orientationUnspecified}if direction < 1 || direction > 8 {// EXIF 规范定义的图像方向值应该在 1 到 8 之间return orientationUnspecified}return orientation(direction)}return orientationUnspecified
}
3、测试
3.1、fs_test.go
package imagesimport ("fmt""testing"
)func TestOpen(t *testing.T) {fileName := "test.jpeg"_, err := Open(fileName)if err != nil {fmt.Println(err)return}fmt.Println(fileName, "读取成功")
}func TestClose(t *testing.T) {fileName := "test.jpeg"quality := 50img, err := Open(fileName)if err != nil {fmt.Println(err)return}err = Close(img, "compress_"+fileName, quality)if err != nil {fmt.Println(err)return}fmt.Println(fileName, "保存成功")
}
4、发布
创建一个新的tag:v0.0.1
5、使用
go get -u github.com/xumeng03/images
package mainimport ("fmt""github.com/xumeng03/images"
)func main() {fileName := "test.jpeg"quality := 50img, err := images.Open(fileName)if err != nil {fmt.Println(err)return}err = images.Close(img, "compress_"+fileName, quality)if err != nil {fmt.Println(err)return}fmt.Println(fileName, "压缩成功")
}