无损压缩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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@@ -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