Files
VptPassiveAdapter/util/image.go
Jerry Yan 0b42dad969 feat(api): 添加图像处理配置和服务器端图像处理功能
- 新增 CompressionConfig、ThumbnailConfig 和 ImageProcessingConfig 结构体用于图像处理配置
- 实现 GetEffectiveConfig 方法提供图像处理配置的默认值
- 在 UploadConfig 中添加 ImageProcessing 字段传递服务器配置
- 移除客户端本地缩略图生成功能,改用服务器端处理
- 添加 UploadFaceDataWithProcessing 函数实现带图像处理的上传流程
- 实现 configToImageOptions 函数将服务器配置转换为图像处理选项
- 在 util/image.go 中添加完整的图像处理功能,支持裁切和缩放模式
- 更新依赖添加 golang.org/x/image 用于高质量图像缩放
- 添加 .claude 到 .gitignore 文件
2025-12-30 11:51:17 +08:00

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)
}