- 新增 S3 兼容 Storage 抽象与 MinIO 实现,compose 启动 MinIO - 上传接口:校验 → 缩放 → WebP → 主图/缩略图入库 - 消息图片 content 改为对象 URL,拒绝 base64 - 工作台/访客端改为先上传再发消息
107 lines
2.3 KiB
Go
107 lines
2.3 KiB
Go
package media
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image"
|
|
_ "image/gif"
|
|
_ "image/jpeg"
|
|
_ "image/png"
|
|
"io"
|
|
|
|
"github.com/chai2010/webp"
|
|
"github.com/disintegration/imaging"
|
|
)
|
|
|
|
const (
|
|
DefaultMaxEdge = 1920
|
|
DefaultThumbEdge = 400
|
|
DefaultQuality = 80
|
|
)
|
|
|
|
type ProcessOptions struct {
|
|
MaxEdge int
|
|
ThumbEdge int
|
|
WebPQuality float32
|
|
}
|
|
|
|
type ProcessedImage struct {
|
|
Main []byte
|
|
MainType string // image/webp
|
|
MainExt string // .webp
|
|
Thumb []byte
|
|
ThumbType string
|
|
ThumbExt string
|
|
Width int
|
|
Height int
|
|
ThumbWidth int
|
|
ThumbHeight int
|
|
}
|
|
|
|
// ProcessImage 解码 → 等比缩放 → WebP 主图 + 缩略图
|
|
func ProcessImage(r io.Reader, opt ProcessOptions) (*ProcessedImage, error) {
|
|
if opt.MaxEdge <= 0 {
|
|
opt.MaxEdge = DefaultMaxEdge
|
|
}
|
|
if opt.ThumbEdge <= 0 {
|
|
opt.ThumbEdge = DefaultThumbEdge
|
|
}
|
|
if opt.WebPQuality <= 0 || opt.WebPQuality > 100 {
|
|
opt.WebPQuality = DefaultQuality
|
|
}
|
|
|
|
src, _, err := image.Decode(r)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("无法解码图片: %w", err)
|
|
}
|
|
|
|
mainImg := fitWithin(src, opt.MaxEdge)
|
|
thumbImg := fitWithin(src, opt.ThumbEdge)
|
|
|
|
mainBuf := &bytes.Buffer{}
|
|
if err := webp.Encode(mainBuf, mainImg, &webp.Options{Quality: opt.WebPQuality}); err != nil {
|
|
return nil, fmt.Errorf("webp 编码失败: %w", err)
|
|
}
|
|
thumbBuf := &bytes.Buffer{}
|
|
if err := webp.Encode(thumbBuf, thumbImg, &webp.Options{Quality: opt.WebPQuality}); err != nil {
|
|
return nil, fmt.Errorf("缩略图编码失败: %w", err)
|
|
}
|
|
|
|
bMain := mainImg.Bounds()
|
|
bThumb := thumbImg.Bounds()
|
|
return &ProcessedImage{
|
|
Main: mainBuf.Bytes(),
|
|
MainType: "image/webp",
|
|
MainExt: ".webp",
|
|
Thumb: thumbBuf.Bytes(),
|
|
ThumbType: "image/webp",
|
|
ThumbExt: ".webp",
|
|
Width: bMain.Dx(),
|
|
Height: bMain.Dy(),
|
|
ThumbWidth: bThumb.Dx(),
|
|
ThumbHeight: bThumb.Dy(),
|
|
}, nil
|
|
}
|
|
|
|
func fitWithin(img image.Image, maxEdge int) image.Image {
|
|
b := img.Bounds()
|
|
w, h := b.Dx(), b.Dy()
|
|
if w <= maxEdge && h <= maxEdge {
|
|
return img
|
|
}
|
|
if w >= h {
|
|
return imaging.Resize(img, maxEdge, 0, imaging.Lanczos)
|
|
}
|
|
return imaging.Resize(img, 0, maxEdge, imaging.Lanczos)
|
|
}
|
|
|
|
// AllowedUploadMIME 上传阶段允许的源格式
|
|
func AllowedUploadMIME(contentType string) bool {
|
|
switch contentType {
|
|
case "image/jpeg", "image/png", "image/gif", "image/webp":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|