无损压缩GIF
This commit is contained in:
52
bin/main.go
52
bin/main.go
@@ -18,17 +18,61 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// 设置日志格式
|
|
||||||
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
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() {
|
func main() {
|
||||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
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) {
|
http.HandleFunc("/img/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// URL 格式: /img/{id}-{version}@{倍图}{宽度}.{格式}
|
// 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)$`)
|
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)
|
matches := reg.FindStringSubmatch(r.URL.Path)
|
||||||
|
@@ -28,7 +28,7 @@ func 生成配置文件() {
|
|||||||
viper.Set("mysql.charset", "utf8mb4")
|
viper.Set("mysql.charset", "utf8mb4")
|
||||||
viper.Set("mysql.maxOpenConns", 100)
|
viper.Set("mysql.maxOpenConns", 100)
|
||||||
|
|
||||||
viper.Set("oss.host", "")
|
viper.Set("oss.endpoint", "")
|
||||||
viper.Set("oss.accessId", "")
|
viper.Set("oss.accessID", "")
|
||||||
viper.Set("oss.accessKey", "")
|
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"
|
_ "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 对象
|
viper := Viper // 从 models/config.go 中获取 viper 对象
|
||||||
user := viper.Get("mysql.user").(string)
|
user := viper.Get("mysql.user").(string)
|
||||||
password := viper.Get("mysql.password").(string)
|
password := viper.Get("mysql.password").(string)
|
||||||
@@ -16,29 +21,17 @@ func GetMysql() {
|
|||||||
port := viper.Get("mysql.port").(int)
|
port := viper.Get("mysql.port").(int)
|
||||||
database := viper.Get("mysql.database").(string)
|
database := viper.Get("mysql.database").(string)
|
||||||
sqlconf := user + ":" + password + "@tcp(" + host + ":" + strconv.Itoa(port) + ")/" + database + "?charset=utf8mb4&parseTime=True&loc=Local"
|
sqlconf := user + ":" + password + "@tcp(" + host + ":" + strconv.Itoa(port) + ")/" + database + "?charset=utf8mb4&parseTime=True&loc=Local"
|
||||||
|
m.db, err = sql.Open("mysql", sqlconf) // 连接数据库
|
||||||
// 连接数据库
|
|
||||||
db, err := sql.Open("mysql", sqlconf)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("连接数据库失败", err)
|
log.Println("连接数据库失败", err)
|
||||||
return
|
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
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
// 遍历查询结果
|
// 根据 id 获取图片的 content
|
||||||
for rows.Next() {
|
func (m *MysqlConnection) GetImageContent(id string) (key string, err error) {
|
||||||
var id, width, height, content string
|
var content string
|
||||||
if err = rows.Scan(&id, &width, &height, &content); err != nil {
|
err = m.db.QueryRow("SELECT content FROM web_images WHERE id=" + id).Scan(&content)
|
||||||
log.Println("读取数据库失败", err)
|
return content, err
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Println(id, width, height, content)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -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