1、初始化go.mod
go mod init github.com/xumeng03/images
2、编写包内容
这里只是一个简单的压缩jpg/jpeg图片例子,代码参考 https://github.com/disintegration/imaging
2.1、fs.go
go
package images
import (
"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
go
package images
import (
"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 orientation
go func() {
defer close(done)
orient = readOrientation(pr)
io.Copy(io.Discard, pr)
}()
img, _, err := image.Decode(reader)
pw.Close()
<-done
fmt.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":
fallthrough
case "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
go
package images
import (
"encoding/binary"
"io"
)
type orientation int
const (
// 方向未指定
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 uint16
if binary.Read(reader, binary.BigEndian, &soi) != nil {
return orientationUnspecified
}
if soi != markerSOI {
return orientationUnspecified
}
for {
var marker, size uint16
if 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 uint32
if 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 uint16
var byteOrder binary.ByteOrder
if err := binary.Read(reader, binary.BigEndian, &byteOrderTag); err != nil {
return orientationUnspecified
}
switch byteOrderTag {
case byteOrderBE:
byteOrder = binary.BigEndian
case byteOrderLE:
byteOrder = binary.LittleEndian
default:
return orientationUnspecified
}
if _, err := io.CopyN(io.Discard, reader, 2); err != nil {
return orientationUnspecified
}
// 跳过 exif 段
var offset uint32
if 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 uint16
if err := binary.Read(reader, byteOrder, &numTags); err != nil {
return orientationUnspecified
}
for i := 0; i < int(numTags); i++ {
var tag uint16
if 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 uint16
if 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
go
package images
import (
"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 := 50
img, 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、使用
shell
go get -u github.com/xumeng03/images
go
package main
import (
"fmt"
"github.com/xumeng03/images"
)
func main() {
fileName := "test.jpeg"
quality := 50
img, 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, "压缩成功")
}