实现 MinIO 对象存储图片流水线(可迁移 OSS)
- 新增 S3 兼容 Storage 抽象与 MinIO 实现,compose 启动 MinIO - 上传接口:校验 → 缩放 → WebP → 主图/缩略图入库 - 消息图片 content 改为对象 URL,拒绝 base64 - 工作台/访客端改为先上传再发消息
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user