init: 初始化 web_hgdjb_server 项目
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
package imgutil
|
||||
|
||||
import "bytes"
|
||||
|
||||
// 与管理后台 ShowImg / imgCode.js 保持一致。
|
||||
var (
|
||||
encryptMagicNumber = []byte{0x88, 0xA8, 0x30, 0xCB, 0x10, 0x76}
|
||||
decryptKey = []byte{50, 48, 49, 57, 121, 115, 97, 112, 112, 55, 53, 50, 55} // 2019ysapp7527
|
||||
plainImageHeaders = [][]byte{
|
||||
{0xFF, 0xD8, 0xFF}, // jpeg
|
||||
{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}, // png
|
||||
{0x47, 0x49, 0x46}, // gif
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
encryptedPrefixLen = 100
|
||||
encryptXORAllKey = 0xA3
|
||||
)
|
||||
|
||||
// DecryptImage 按后台 handleVerAutoImg 的规则解密图床图片。
|
||||
// 已是 jpg/png/gif 则原样返回。
|
||||
func DecryptImage(data []byte) []byte {
|
||||
if len(data) == 0 {
|
||||
return data
|
||||
}
|
||||
out := append([]byte(nil), data...)
|
||||
if hasEncryptMagic(out) {
|
||||
return xorAllLength(out)
|
||||
}
|
||||
if !isPlainImage(out) {
|
||||
xorPrefix(out, decryptKey, encryptedPrefixLen)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func hasEncryptMagic(data []byte) bool {
|
||||
return len(data) >= len(encryptMagicNumber) && bytes.Equal(data[:len(encryptMagicNumber)], encryptMagicNumber)
|
||||
}
|
||||
|
||||
func isPlainImage(data []byte) bool {
|
||||
for _, header := range plainImageHeaders {
|
||||
if len(data) >= len(header) && bytes.Equal(data[:len(header)], header) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func xorPrefix(src, key []byte, length int) {
|
||||
if len(key) == 0 || len(src) == 0 {
|
||||
return
|
||||
}
|
||||
if length <= 0 || length > len(src) {
|
||||
length = len(src)
|
||||
}
|
||||
for i := 0; i < length; {
|
||||
for j := 0; j < len(key) && i < length; j++ {
|
||||
src[i] ^= key[j]
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func xorAllLength(src []byte) []byte {
|
||||
out := make([]byte, 0, len(src))
|
||||
for i, b := range src {
|
||||
if i < len(encryptMagicNumber) && b == encryptMagicNumber[i] {
|
||||
continue
|
||||
}
|
||||
out = append(out, b^encryptXORAllKey)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user