You've already forked VptPassiveAdapter
- 新增 CompressionConfig、ThumbnailConfig 和 ImageProcessingConfig 结构体用于图像处理配置 - 实现 GetEffectiveConfig 方法提供图像处理配置的默认值 - 在 UploadConfig 中添加 ImageProcessing 字段传递服务器配置 - 移除客户端本地缩略图生成功能,改用服务器端处理 - 添加 UploadFaceDataWithProcessing 函数实现带图像处理的上传流程 - 实现 configToImageOptions 函数将服务器配置转换为图像处理选项 - 在 util/image.go 中添加完整的图像处理功能,支持裁切和缩放模式 - 更新依赖添加 golang.org/x/image 用于高质量图像缩放 - 添加 .claude 到 .gitignore 文件
243 lines
7.1 KiB
Go
243 lines
7.1 KiB
Go
package util
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image"
|
|
sdraw "image/draw" // 标准库draw,用于裁切
|
|
"image/jpeg"
|
|
_ "image/png" // Register PNG decoder just in case
|
|
|
|
xdraw "golang.org/x/image/draw" // 扩展库draw,用于高质量缩放
|
|
)
|
|
|
|
// ImageProcessingOptions 图像处理选项
|
|
type ImageProcessingOptions struct {
|
|
// 原图压缩
|
|
SourceCompressionEnabled bool
|
|
SourceQuality int // 1-100,100为不压缩
|
|
|
|
// 缩略图配置
|
|
ThumbnailMode string // "cut"(裁切) 或 "shrink"(缩放)
|
|
ThumbnailShrinkRatio float64 // 缩放/裁切比例 (0-1)
|
|
ThumbnailQuality int // 缩略图压缩质量 (1-100)
|
|
}
|
|
|
|
// DefaultImageProcessingOptions 返回默认的图像处理选项
|
|
// 默认值:原图不压缩;缩略图裁切模式,比例0.5,质量75
|
|
func DefaultImageProcessingOptions() *ImageProcessingOptions {
|
|
return &ImageProcessingOptions{
|
|
SourceCompressionEnabled: false,
|
|
SourceQuality: 100,
|
|
ThumbnailMode: "cut",
|
|
ThumbnailShrinkRatio: 0.5,
|
|
ThumbnailQuality: 75,
|
|
}
|
|
}
|
|
|
|
// ProcessedImages 处理后的图像结果
|
|
type ProcessedImages struct {
|
|
FaceData []byte // 人脸图(不压缩)
|
|
ThumbData []byte // 缩略图
|
|
SourceData []byte // 原图(可能压缩)
|
|
}
|
|
|
|
// ProcessImages 根据配置处理所有图像
|
|
// faceData: 人脸图数据
|
|
// srcData: 原图数据
|
|
// faceRect: 人脸区域坐标 (leftTopX, leftTopY, rightBtmX, rightBtmY)
|
|
// opts: 处理选项(如果为nil,使用默认值)
|
|
func ProcessImages(faceData, srcData []byte, faceRect [4]int, opts *ImageProcessingOptions) (*ProcessedImages, error) {
|
|
if opts == nil {
|
|
opts = DefaultImageProcessingOptions()
|
|
}
|
|
|
|
result := &ProcessedImages{}
|
|
|
|
// 1. 人脸图不压缩,直接使用原始数据
|
|
result.FaceData = faceData
|
|
|
|
// 2. 处理原图(根据配置决定是否压缩)
|
|
if len(srcData) > 0 {
|
|
var err error
|
|
if opts.SourceCompressionEnabled && opts.SourceQuality < 100 {
|
|
result.SourceData, err = CompressImage(srcData, opts.SourceQuality)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("compress source image failed: %v", err)
|
|
}
|
|
} else {
|
|
result.SourceData = srcData
|
|
}
|
|
}
|
|
|
|
// 3. 生成缩略图
|
|
if len(srcData) > 0 {
|
|
var err error
|
|
switch opts.ThumbnailMode {
|
|
case "shrink":
|
|
// 整体缩放模式
|
|
result.ThumbData, err = GenerateThumbnailShrink(srcData, opts.ThumbnailShrinkRatio, opts.ThumbnailQuality)
|
|
default: // "cut" 或其他默认为裁切模式
|
|
// 以人脸为中心裁切
|
|
result.ThumbData, err = GenerateThumbnailCut(srcData, faceRect, opts.ThumbnailShrinkRatio, opts.ThumbnailQuality)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("generate thumbnail failed: %v", err)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// CompressImage 压缩图片到指定质量
|
|
func CompressImage(srcData []byte, quality int) ([]byte, error) {
|
|
img, _, err := image.Decode(bytes.NewReader(srcData))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode image failed: %v", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encode image failed: %v", err)
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// GenerateThumbnailShrink 整体缩放模式生成缩略图
|
|
// ratio: 缩放比例(0-1),如0.5表示缩放到原图50%大小
|
|
// quality: JPEG压缩质量(1-100)
|
|
func GenerateThumbnailShrink(srcData []byte, ratio float64, quality int) ([]byte, error) {
|
|
img, _, err := image.Decode(bytes.NewReader(srcData))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode image failed: %v", err)
|
|
}
|
|
|
|
bounds := img.Bounds()
|
|
origW := bounds.Dx()
|
|
origH := bounds.Dy()
|
|
|
|
// 计算目标尺寸
|
|
targetW := int(float64(origW) * ratio)
|
|
targetH := int(float64(origH) * ratio)
|
|
|
|
if targetW == 0 || targetH == 0 {
|
|
return nil, fmt.Errorf("image too small to generate thumbnail with ratio %.2f", ratio)
|
|
}
|
|
|
|
// 创建目标图像并使用高质量缩放
|
|
dst := image.NewRGBA(image.Rect(0, 0, targetW, targetH))
|
|
xdraw.CatmullRom.Scale(dst, dst.Bounds(), img, bounds, xdraw.Over, nil)
|
|
|
|
// 编码为JPEG
|
|
var buf bytes.Buffer
|
|
err = jpeg.Encode(&buf, dst, &jpeg.Options{Quality: quality})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encode thumbnail failed: %v", err)
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// GenerateThumbnailCut 以人脸为中心裁切模式生成缩略图
|
|
// faceRect: 人脸区域坐标 [leftTopX, leftTopY, rightBtmX, rightBtmY]
|
|
// ratio: 裁切比例(0-1),裁切区域为原图尺寸的ratio倍
|
|
// quality: JPEG压缩质量(1-100)
|
|
func GenerateThumbnailCut(srcData []byte, faceRect [4]int, ratio float64, quality int) ([]byte, error) {
|
|
img, _, err := image.Decode(bytes.NewReader(srcData))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode image failed: %v", err)
|
|
}
|
|
|
|
bounds := img.Bounds()
|
|
origW := bounds.Dx()
|
|
origH := bounds.Dy()
|
|
|
|
// 计算目标尺寸(原图的ratio倍)
|
|
targetW := int(float64(origW) * ratio)
|
|
targetH := int(float64(origH) * ratio)
|
|
|
|
if targetW == 0 || targetH == 0 {
|
|
return nil, fmt.Errorf("image too small to generate thumbnail with ratio %.2f", ratio)
|
|
}
|
|
|
|
// 计算人脸中心
|
|
centerX := (faceRect[0] + faceRect[2]) / 2
|
|
centerY := (faceRect[1] + faceRect[3]) / 2
|
|
|
|
// 如果人脸坐标无效,使用图片中心
|
|
if centerX <= 0 || centerY <= 0 {
|
|
centerX = origW / 2
|
|
centerY = origH / 2
|
|
}
|
|
|
|
// 计算裁切起点(以人脸中心为中心)
|
|
cropX := centerX - targetW/2
|
|
cropY := centerY - targetH/2
|
|
|
|
// 约束到图像边界
|
|
if cropX < 0 {
|
|
cropX = 0
|
|
}
|
|
if cropY < 0 {
|
|
cropY = 0
|
|
}
|
|
if cropX+targetW > origW {
|
|
cropX = origW - targetW
|
|
}
|
|
if cropY+targetH > origH {
|
|
cropY = origH - targetH
|
|
}
|
|
|
|
// 再次检查边界(处理图像尺寸小于目标尺寸的情况)
|
|
if cropX < 0 {
|
|
cropX = 0
|
|
}
|
|
if cropY < 0 {
|
|
cropY = 0
|
|
}
|
|
|
|
// 执行裁切
|
|
cropRect := image.Rect(cropX, cropY, cropX+targetW, cropY+targetH)
|
|
|
|
var dst image.Image
|
|
|
|
// 尝试使用SubImage优化性能
|
|
type SubImager interface {
|
|
SubImage(r image.Rectangle) image.Image
|
|
}
|
|
|
|
if sub, ok := img.(SubImager); ok {
|
|
dst = sub.SubImage(cropRect)
|
|
} else {
|
|
// 回退方案:创建新图像并绘制
|
|
rgba := image.NewRGBA(image.Rect(0, 0, targetW, targetH))
|
|
sdraw.Draw(rgba, rgba.Bounds(), img, cropRect.Min, sdraw.Src)
|
|
dst = rgba
|
|
}
|
|
|
|
// 编码为JPEG
|
|
var buf bytes.Buffer
|
|
err = jpeg.Encode(&buf, dst, &jpeg.Options{Quality: quality})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encode thumbnail failed: %v", err)
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// GenerateThumbnail creates a thumbnail by cropping the original image.
|
|
// The thumbnail size is 1/2 of the original dimensions.
|
|
// The crop is centered on the provided coordinates (centerX, centerY), constrained to image bounds.
|
|
// 已废弃:请使用 GenerateThumbnailCut 或 GenerateThumbnailShrink
|
|
func GenerateThumbnail(srcData []byte, centerX, centerY int) ([]byte, error) {
|
|
return GenerateThumbnailCut(srcData, [4]int{centerX, centerY, centerX, centerY}, 0.5, 85)
|
|
}
|
|
|
|
// GenerateThumbnailFromCoords Helper to process using LeftTop and RightBottom coordinates
|
|
// 已废弃:请使用 ProcessImages
|
|
func GenerateThumbnailFromCoords(srcData []byte, ltX, ltY, rbX, rbY int) ([]byte, error) {
|
|
return GenerateThumbnailCut(srcData, [4]int{ltX, ltY, rbX, rbY}, 0.5, 85)
|
|
}
|