From 0b50f908ab66fc1978325b9176eb1032489ebd45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A1=9C=E8=8F=AF?= Date: Sat, 8 Apr 2023 13:14:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=A0=E6=8D=9F=E5=8E=8B=E7=BC=A9GIF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/main.go | 52 ++++++++++++++++++-- models/config.go | 4 +- models/image.go | 121 +++++++++++++++++++++++++++++++++++++++++++++++ models/mysql.go | 37 ++++++--------- models/resnet.go | 55 --------------------- 5 files changed, 186 insertions(+), 83 deletions(-) create mode 100644 models/image.go delete mode 100644 models/resnet.go diff --git a/bin/main.go b/bin/main.go index 8314a9e..7f72caf 100644 --- a/bin/main.go +++ b/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) diff --git a/models/config.go b/models/config.go index bce046e..07949df 100644 --- a/models/config.go +++ b/models/config.go @@ -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", "") } diff --git a/models/image.go b/models/image.go new file mode 100644 index 0000000..8632517 --- /dev/null +++ b/models/image.go @@ -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) + } +} diff --git a/models/mysql.go b/models/mysql.go index a619d65..a81bd3f 100644 --- a/models/mysql.go +++ b/models/mysql.go @@ -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) - } + return +} + +// 根据 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 } diff --git a/models/resnet.go b/models/resnet.go deleted file mode 100644 index 637fa14..0000000 --- a/models/resnet.go +++ /dev/null @@ -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()) - **/ -}