Files
2026-10-26 16:28:14 +08:00

152 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package imgutil
import (
"bytes"
"crypto/sha1"
"fmt"
"image"
_ "image/gif"
"image/jpeg"
_ "image/png"
"net/url"
"path"
"path/filepath"
"strings"
"unicode"
)
const (
// CoverResizeLimitKB 与 media utils.Resize 一致:超过该大小才缩放
CoverResizeLimitKB = 60
// CoverMaxWidth 与 media Thumbnail 一致
CoverMaxWidth = 750
// CoverMaxHeight 与 media Thumbnail 一致
CoverMaxHeight = 1560
// CoverJPEGQuality 压缩后统一输出 JPEG
CoverJPEGQuality = 75
)
// IsGIF 按文件头判断是否为 GIF,不依赖扩展名。
func IsGIF(data []byte) bool {
return len(data) >= 3 && bytes.Equal(data[:3], []byte{0x47, 0x49, 0x46})
}
// NeedCompress 原图是否超过 media Resize 阈值,未超过则无需压缩重传。
// GIF 保持原格式和动效,不参与压缩。
func NeedCompress(data []byte) bool {
if IsGIF(data) {
return false
}
return int64(len(data))/1024 > CoverResizeLimitKB
}
// CompressCover 按 media Resize(60KB, 750×1560) 压缩封面。
// GIF 原样返回;其它格式未超过阈值时原样返回,超过则缩放后统一输出 JPEG。
func CompressCover(data []byte, fileName string) ([]byte, string, error) {
if len(data) == 0 {
return nil, "", fmt.Errorf("图片内容为空")
}
outName := compressFileName(fileName)
if IsGIF(data) {
return data, withExt(outName, ".gif"), nil
}
if !NeedCompress(data) {
return data, outName, nil
}
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return nil, "", fmt.Errorf("解码图片失败: %w", err)
}
thumb := thumbnail(img, CoverMaxWidth, CoverMaxHeight)
var buf bytes.Buffer
if err = jpeg.Encode(&buf, thumb, &jpeg.Options{Quality: CoverJPEGQuality}); err != nil {
return nil, "", err
}
return buf.Bytes(), withExt(outName, ".jpg"), nil
}
// SafeCoverFileName 生成仅含 ASCII 的上传文件名。
// 原封面路径含中文时,直接用 path.Base 会把中文带进上传名,图床返回地址会异常。
func SafeCoverFileName(src string) string {
src = strings.TrimSpace(src)
ext := asciiImageExt(src)
if src == "" {
return "playlet_cover" + ext
}
sum := sha1.Sum([]byte(src))
return fmt.Sprintf("playlet_cover_%x%s", sum[:8], ext)
}
func thumbnail(img image.Image, maxW, maxH int) image.Image {
b := img.Bounds()
w, h := b.Dx(), b.Dy()
if w <= maxW && h <= maxH {
return img
}
wr := float64(maxW) / float64(w)
hr := float64(maxH) / float64(h)
scale := wr
if hr < wr {
scale = hr
}
nw := int(float64(w) * scale)
nh := int(float64(h) * scale)
if nw < 1 {
nw = 1
}
if nh < 1 {
nh = 1
}
dst := image.NewRGBA(image.Rect(0, 0, nw, nh))
for y := 0; y < nh; y++ {
sy := b.Min.Y + y*h/nh
for x := 0; x < nw; x++ {
sx := b.Min.X + x*w/nw
dst.Set(x, y, img.At(sx, sy))
}
}
return dst
}
func compressFileName(fileName string) string {
fileName = filepath.Base(strings.TrimSpace(fileName))
if fileName == "" || fileName == "." || fileName == string(filepath.Separator) || !isASCIIFileName(fileName) {
return "playlet_cover.jpg"
}
return fileName
}
func asciiImageExt(src string) string {
name := src
if u, err := url.Parse(src); err == nil && u.Path != "" {
name = u.Path
}
name = strings.Split(path.Base(name), "?")[0]
if dec, err := url.PathUnescape(name); err == nil {
name = dec
}
ext := strings.ToLower(filepath.Ext(name))
switch ext {
case ".jpg", ".jpeg", ".png", ".gif", ".webp":
return ext
default:
return ".jpg"
}
}
func isASCIIFileName(name string) bool {
if !strings.Contains(name, ".") {
return false
}
for _, r := range name {
if r > unicode.MaxASCII || r < 32 || r == '"' || r == '/' || r == '\\' {
return false
}
}
return true
}
func withExt(fileName, ext string) string {
return strings.TrimSuffix(fileName, filepath.Ext(fileName)) + ext
}