无损压缩GIF
This commit is contained in:
		
							
								
								
									
										52
									
								
								bin/main.go
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								bin/main.go
									
									
									
									
									
								
							@@ -18,17 +18,61 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	// 设置日志格式
 | 
			
		||||
	log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 实现一个 web api 服务(获取指定尺寸的图片)
 | 
			
		||||
// string 转换为 int, 如果转换失败则返回默认值
 | 
			
		||||
func stringToInt(str string, defaultValue int) int {
 | 
			
		||||
	if str == "" {
 | 
			
		||||
		return defaultValue
 | 
			
		||||
	}
 | 
			
		||||
	value, err := strconv.Atoi(str)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return defaultValue
 | 
			
		||||
	}
 | 
			
		||||
	return value
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	runtime.GOMAXPROCS(runtime.NumCPU())
 | 
			
		||||
	models.GetMysql() // 测试连接数据库
 | 
			
		||||
 | 
			
		||||
	var mysqlConnection models.MysqlConnection
 | 
			
		||||
	mysqlConnection.Init()
 | 
			
		||||
 | 
			
		||||
	// 直接走 CDN 的请求不缓存本地, 因此直接使用参数
 | 
			
		||||
	http.HandleFunc("/webp/", func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		log.Println(r.Method, r.URL.Path)
 | 
			
		||||
		// URL 格式: /img/{id}.{格式}?width=320&height=320&fit=cover
 | 
			
		||||
		reg := regexp.MustCompile(`^/webp/([0-9a-zA-Z]+).(jpg|jpeg|png|webp)$`)
 | 
			
		||||
		matches := reg.FindStringSubmatch(r.URL.Path)
 | 
			
		||||
		if len(matches) != 3 {
 | 
			
		||||
			log.Println("URL 格式错误", matches)
 | 
			
		||||
			w.WriteHeader(http.StatusNotFound)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		id, format, width, height, fit := matches[1], matches[2], stringToInt(r.URL.Query().Get("width"), 0), stringToInt(r.URL.Query().Get("height"), 0), r.URL.Query().Get("fit")
 | 
			
		||||
		content, err := mysqlConnection.GetImageContent(id)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println("获取图片失败", format, err)
 | 
			
		||||
			w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var img models.Image
 | 
			
		||||
		img.Init(content)
 | 
			
		||||
 | 
			
		||||
		data, err := img.ToWebP(width, height, fit)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println("转换图片失败", err)
 | 
			
		||||
			w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		w.Header().Set("Content-Type", "image/webp")
 | 
			
		||||
		w.Header().Set("Cache-Control", "max-age=31536000")
 | 
			
		||||
		w.Write(data)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	http.HandleFunc("/img/", func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
 | 
			
		||||
		// URL 格式: /img/{id}-{version}@{倍图}{宽度}.{格式}
 | 
			
		||||
		reg := regexp.MustCompile(`^/img/([0-9a-zA-Z]+)-([0-9a-zA-Z]+)@([0-9]{1,4})x([0-9]{1,4}).(jpg|jpeg|png|webp)$`)
 | 
			
		||||
		matches := reg.FindStringSubmatch(r.URL.Path)
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ func 生成配置文件() {
 | 
			
		||||
	viper.Set("mysql.charset", "utf8mb4")
 | 
			
		||||
	viper.Set("mysql.maxOpenConns", 100)
 | 
			
		||||
 | 
			
		||||
	viper.Set("oss.host", "")
 | 
			
		||||
	viper.Set("oss.accessId", "")
 | 
			
		||||
	viper.Set("oss.endpoint", "")
 | 
			
		||||
	viper.Set("oss.accessID", "")
 | 
			
		||||
	viper.Set("oss.accessKey", "")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										121
									
								
								models/image.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								models/image.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,121 @@
 | 
			
		||||
package models
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"image"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"regexp"
 | 
			
		||||
 | 
			
		||||
	"github.com/chai2010/webp"
 | 
			
		||||
	"github.com/disintegration/imaging"
 | 
			
		||||
	giftowebp "github.com/sizeofint/gif-to-webp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Image is a struct that contains the information of an image
 | 
			
		||||
type Image struct {
 | 
			
		||||
	image  image.Image
 | 
			
		||||
	format string
 | 
			
		||||
	data   []byte
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 初始化图片
 | 
			
		||||
func (img *Image) Init(content string) {
 | 
			
		||||
	if len(regexp.MustCompile(`image.gameuiux.cn`).FindStringSubmatch(content)) > 0 {
 | 
			
		||||
		key := regexp.MustCompile(`^https?://image.gameuiux.cn/`).ReplaceAllString(content, "")
 | 
			
		||||
 | 
			
		||||
		// 从OSS中读取图片
 | 
			
		||||
		bucket := GetBucket("gameui-image2")
 | 
			
		||||
		body, err := bucket.GetObject(key)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println("读取图片失败", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		defer body.Close()
 | 
			
		||||
 | 
			
		||||
		// 判断图片格式是否为 gif 或 GIF
 | 
			
		||||
		if len(regexp.MustCompile(`\.gif$`).FindStringSubmatch(key)) > 0 {
 | 
			
		||||
			img.format = "gif"
 | 
			
		||||
			img.data, err = ioutil.ReadAll(body)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Println("读取图片失败", err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 将文件解码为 image.Image
 | 
			
		||||
		img.image, img.format, err = image.Decode(body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println("解码图像失败", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	} else {
 | 
			
		||||
		log.Println("直接从网络下载图片:", content)
 | 
			
		||||
		resp, err := http.Get(content)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println("下载图片失败", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
		// 判断图片格式是否为 gif 或 GIF
 | 
			
		||||
		if len(regexp.MustCompile(`\.gif$`).FindStringSubmatch(content)) > 0 {
 | 
			
		||||
			img.format = "gif"
 | 
			
		||||
			img.data, err = ioutil.ReadAll(resp.Body)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Println("读取图片失败", err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			println("数据长度:", len(img.data))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 将文件解码为 image.Image
 | 
			
		||||
		img.image, img.format, err = image.Decode(resp.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println("解码图像失败", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 将图片输出为指定尺寸的 webp 格式(默认使用 Lanczos 缩放算法)
 | 
			
		||||
func (img *Image) ToWebP(width int, height int, fit string) ([]byte, error) {
 | 
			
		||||
	// 如果原图是GIF格式的动态图片,直接不作尺寸处理,直接转换为webp格式
 | 
			
		||||
	if img.format == "gif" {
 | 
			
		||||
		converter := giftowebp.NewConverter()
 | 
			
		||||
		converter.LoopCompatibility = true   // 是否兼容循环动画
 | 
			
		||||
		converter.WebPConfig.SetLossless(1)  // 0 有损压缩 1无损压缩
 | 
			
		||||
		converter.WebPConfig.SetMethod(6)    // 压缩速度 0-6 0最快 6质量最好
 | 
			
		||||
		converter.WebPConfig.SetQuality(100) // 压缩质量 0-100
 | 
			
		||||
		converter.WebPAnimEncoderOptions.SetKmin(9)
 | 
			
		||||
		converter.WebPAnimEncoderOptions.SetKmax(17)
 | 
			
		||||
		return converter.Convert(img.data)
 | 
			
		||||
	}
 | 
			
		||||
	// 如果指定了宽高却没有指定fit, 则默认fit为cover
 | 
			
		||||
	if width != 0 && height != 0 && fit == "" {
 | 
			
		||||
		fit = "cover"
 | 
			
		||||
	}
 | 
			
		||||
	// 如果未指定宽高和fit, 则不缩放图片直接返回webp
 | 
			
		||||
	if width == 0 && height == 0 && fit == "" {
 | 
			
		||||
		return webp.EncodeRGBA(img.image, 100)
 | 
			
		||||
	}
 | 
			
		||||
	switch fit {
 | 
			
		||||
	case "cover":
 | 
			
		||||
		return webp.EncodeRGBA(imaging.Fill(img.image, width, height, imaging.Center, imaging.Lanczos), 100)
 | 
			
		||||
	case "contain":
 | 
			
		||||
		return webp.EncodeRGBA(imaging.Fit(img.image, width, height, imaging.Lanczos), 100)
 | 
			
		||||
	case "fill":
 | 
			
		||||
		return webp.EncodeRGBA(imaging.Fill(img.image, width, height, imaging.Center, imaging.Lanczos), 100)
 | 
			
		||||
	case "inside":
 | 
			
		||||
		return webp.EncodeRGBA(imaging.Fit(img.image, width, height, imaging.Lanczos), 100)
 | 
			
		||||
	case "outside":
 | 
			
		||||
		return webp.EncodeRGBA(imaging.Fill(img.image, width, height, imaging.Center, imaging.Lanczos), 100)
 | 
			
		||||
	case "scale-down":
 | 
			
		||||
		return webp.EncodeRGBA(imaging.Fit(img.image, width, height, imaging.Lanczos), 100)
 | 
			
		||||
	default:
 | 
			
		||||
		return webp.EncodeRGBA(imaging.Resize(img.image, width, height, imaging.Lanczos), 100)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -8,7 +8,12 @@ import (
 | 
			
		||||
	_ "github.com/go-sql-driver/mysql"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetMysql() {
 | 
			
		||||
type MysqlConnection struct {
 | 
			
		||||
	db *sql.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 初始化数据库连接
 | 
			
		||||
func (m *MysqlConnection) Init() (err error) {
 | 
			
		||||
	viper := Viper // 从 models/config.go 中获取 viper 对象
 | 
			
		||||
	user := viper.Get("mysql.user").(string)
 | 
			
		||||
	password := viper.Get("mysql.password").(string)
 | 
			
		||||
@@ -16,29 +21,17 @@ func GetMysql() {
 | 
			
		||||
	port := viper.Get("mysql.port").(int)
 | 
			
		||||
	database := viper.Get("mysql.database").(string)
 | 
			
		||||
	sqlconf := user + ":" + password + "@tcp(" + host + ":" + strconv.Itoa(port) + ")/" + database + "?charset=utf8mb4&parseTime=True&loc=Local"
 | 
			
		||||
 | 
			
		||||
	// 连接数据库
 | 
			
		||||
	db, err := sql.Open("mysql", sqlconf)
 | 
			
		||||
	m.db, err = sql.Open("mysql", sqlconf) // 连接数据库
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Println("连接数据库失败", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 从 web_images 表读取图片 id, version, width, height, format
 | 
			
		||||
	rows, err := db.Query("SELECT id, width, height, content FROM web_images WHERE id = 8888")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Println("查询数据库失败", err)
 | 
			
		||||
	return
 | 
			
		||||
	}
 | 
			
		||||
	defer rows.Close()
 | 
			
		||||
 | 
			
		||||
	// 遍历查询结果
 | 
			
		||||
	for rows.Next() {
 | 
			
		||||
		var id, width, height, content string
 | 
			
		||||
		if err = rows.Scan(&id, &width, &height, &content); err != nil {
 | 
			
		||||
			log.Println("读取数据库失败", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		log.Println(id, width, height, content)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 根据 id 获取图片的 content
 | 
			
		||||
func (m *MysqlConnection) GetImageContent(id string) (key string, err error) {
 | 
			
		||||
	var content string
 | 
			
		||||
	err = m.db.QueryRow("SELECT content FROM web_images WHERE id=" + id).Scan(&content)
 | 
			
		||||
	return content, err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,55 +0,0 @@
 | 
			
		||||
package models
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	//"gocv.io/x/gocv"
 | 
			
		||||
	//"github.com/xuyu/gotool/torch"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/wangkuiyi/gotorch"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	// 模型文件地址: https://download.pytorch.org/models/resnet50-19c8e357.pth
 | 
			
		||||
	// 模型地址: "/home/satori/webp/data/resnet-50.t7"
 | 
			
		||||
 | 
			
		||||
	tensor := gotorch.Load("/home/satori/webp/data/resnet-50.t7")
 | 
			
		||||
	fmt.Println(tensor)
 | 
			
		||||
 | 
			
		||||
	//model := torch.NewModel()
 | 
			
		||||
	//err := model.ReadFromFile("/home/satori/webp/data/resnet-50.t7")
 | 
			
		||||
	//if err != nil {
 | 
			
		||||
	//	panic(err)
 | 
			
		||||
	//}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	t7 := "/home/satori/webp/data/resnet-50.t7"
 | 
			
		||||
	// 加载t7格式的模型
 | 
			
		||||
	model := gocv.ReadNetFromTorch(t7)
 | 
			
		||||
 | 
			
		||||
	if model.Empty() {
 | 
			
		||||
		panic("Failed to load model")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fmt.Println("==============================")
 | 
			
		||||
	img := gocv.IMRead("data/test.jpeg", gocv.IMReadColor)
 | 
			
		||||
	if img.Empty() {
 | 
			
		||||
		panic("Failed to read image")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fmt.Println("==============================")
 | 
			
		||||
	inputBlob := gocv.BlobFromImage(img, 1.0, image.Pt(224, 224), gocv.NewScalar(0, 0, 0, 0), true, false)
 | 
			
		||||
	defer inputBlob.Close()
 | 
			
		||||
 | 
			
		||||
	fmt.Println("==============================")
 | 
			
		||||
	model.SetInput(inputBlob, "input")
 | 
			
		||||
	outputBlob := model.Forward("output")
 | 
			
		||||
	defer outputBlob.Close()
 | 
			
		||||
 | 
			
		||||
	fmt.Println("==============================")
 | 
			
		||||
	features := outputBlob.Reshape(1, 1)
 | 
			
		||||
	defer features.Close()
 | 
			
		||||
 | 
			
		||||
	fmt.Println("==============================")
 | 
			
		||||
	fmt.Println(features.ToBytes())
 | 
			
		||||
	**/
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user