无损压缩GIF

This commit is contained in:
2023-04-08 13:14:48 +08:00
parent feeea59c4b
commit 0b50f908ab
5 changed files with 186 additions and 83 deletions

View File

@@ -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)

View File

@@ -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
View 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)
}
}

View File

@@ -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
}

View File

@@ -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())
**/
}