package service import ( "bytes" "carrotskin/internal/model" "carrotskin/internal/repository" "carrotskin/internal/service/skin_renderer" "carrotskin/pkg/database" "carrotskin/pkg/storage" "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "image" "image/color" "image/draw" "image/png" "io" "net/http" "time" "github.com/chai2010/webp" "go.uber.org/zap" ) // textureRenderService TextureRenderService的实现 type textureRenderService struct { textureRepo repository.TextureRepository storage *storage.StorageClient cache *database.CacheManager cacheKeys *database.CacheKeyBuilder logger *zap.Logger minecraft *skin_renderer.Minecraft // 3D 渲染器 } // NewTextureRenderService 创建TextureRenderService实例 func NewTextureRenderService( textureRepo repository.TextureRepository, storageClient *storage.StorageClient, cacheManager *database.CacheManager, logger *zap.Logger, ) TextureRenderService { return &textureRenderService{ textureRepo: textureRepo, storage: storageClient, cache: cacheManager, cacheKeys: database.NewCacheKeyBuilder(""), logger: logger, minecraft: skin_renderer.NewMinecraft(), } } // RenderTexture 渲染纹理为预览图 func (s *textureRenderService) RenderTexture(ctx context.Context, textureID int64, renderType RenderType, size int, format ImageFormat) (*RenderResult, error) { // 参数验证 if size <= 0 || size > 2048 { return nil, errors.New("渲染尺寸必须在1到2048之间") } contentType, err := normalizeFormat(format) if err != nil { return nil, err } // 检查缓存(包含格式) cacheKey := s.cacheKeys.TextureRender(textureID, fmt.Sprintf("%s:%s", renderType, format), size) var cached RenderResult if err := s.cache.Get(ctx, cacheKey, &cached); err == nil && cached.URL != "" { return &cached, nil } // 获取纹理信息 texture, err := s.textureRepo.FindByID(ctx, textureID) if err != nil { return nil, fmt.Errorf("获取纹理失败: %w", err) } if texture == nil { return nil, errors.New("纹理不存在") } // 从对象存储获取纹理文件 textureData, srcInfo, err := s.downloadTexture(ctx, texture.URL) if err != nil { return nil, fmt.Errorf("下载纹理失败: %w", err) } // 渲染纹理 renderedImage, _, err := s.RenderTextureFromData(ctx, textureData, renderType, size, format, texture.IsSlim) if err != nil { return nil, fmt.Errorf("渲染纹理失败: %w", err) } // 保存渲染结果到对象存储 result, err := s.saveRenderToStorage(ctx, textureID, texture.Hash, renderType, size, format, renderedImage, contentType) if err != nil { return nil, fmt.Errorf("保存渲染结果失败: %w", err) } // 若源对象有元信息,透传 LastModified/ETag 作为参考 if srcInfo != nil { if result.LastModified.IsZero() { result.LastModified = srcInfo.LastModified } if result.ETag == "" { result.ETag = srcInfo.ETag } } // 缓存结果(1小时) if err := s.cache.Set(ctx, cacheKey, result, time.Hour); err != nil { s.logger.Warn("缓存渲染结果失败", zap.Error(err)) } return result, nil } // RenderAvatar 渲染头像(支持2D/3D模式) func (s *textureRenderService) RenderAvatar(ctx context.Context, textureID int64, size int, mode AvatarMode, format ImageFormat) (*RenderResult, error) { if size <= 0 || size > 1024 { return nil, errors.New("头像渲染尺寸必须在1到1024之间") } contentType, err := normalizeFormat(format) if err != nil { return nil, err } renderKey := fmt.Sprintf("avatar-%s", mode) cacheKey := s.cacheKeys.TextureRender(textureID, fmt.Sprintf("%s:%s", renderKey, format), size) var cached RenderResult if err := s.cache.Get(ctx, cacheKey, &cached); err == nil && cached.URL != "" { return &cached, nil } texture, err := s.textureRepo.FindByID(ctx, textureID) if err != nil { return nil, fmt.Errorf("获取纹理失败: %w", err) } if texture == nil { return nil, errors.New("纹理不存在") } if texture.Type != model.TextureTypeSkin { return nil, errors.New("仅皮肤纹理支持头像渲染") } textureData, srcInfo, err := s.downloadTexture(ctx, texture.URL) if err != nil { return nil, fmt.Errorf("下载纹理失败: %w", err) } // 使用新的 3D 渲染器 var rendered image.Image switch mode { case AvatarMode3D: // 使用 Blessing Skin 风格的 3D 头像渲染 ratio := float64(size) / 15.0 // 基准比例 rendered, err = s.minecraft.Render3DAvatar(textureData, ratio) if err != nil { s.logger.Warn("3D头像渲染失败,回退到2D", zap.Error(err)) img, decErr := png.Decode(bytes.NewReader(textureData)) if decErr != nil { return nil, fmt.Errorf("解码PNG失败: %w", decErr) } rendered = s.renderHeadView(img, size) } default: // 2D 头像使用新渲染器 ratio := float64(size) / 15.0 rendered, err = s.minecraft.Render2DAvatar(textureData, ratio) if err != nil { s.logger.Warn("2D头像渲染失败,回退到旧方法", zap.Error(err)) img, decErr := png.Decode(bytes.NewReader(textureData)) if decErr != nil { return nil, fmt.Errorf("解码PNG失败: %w", decErr) } rendered = s.renderHeadView(img, size) } } encoded, err := encodeImage(rendered, format) if err != nil { return nil, fmt.Errorf("编码渲染头像失败: %w", err) } result, err := s.saveRenderToStorage(ctx, textureID, texture.Hash, RenderType(renderKey), size, format, encoded, contentType) if err != nil { return nil, fmt.Errorf("保存头像渲染失败: %w", err) } if srcInfo != nil && result.LastModified.IsZero() { result.LastModified = srcInfo.LastModified } if err := s.cache.Set(ctx, cacheKey, result, time.Hour); err != nil { s.logger.Warn("缓存头像渲染失败", zap.Error(err)) } return result, nil } // RenderCape 渲染披风 func (s *textureRenderService) RenderCape(ctx context.Context, textureID int64, size int, format ImageFormat) (*RenderResult, error) { if size <= 0 || size > 2048 { return nil, errors.New("披风渲染尺寸必须在1到2048之间") } contentType, err := normalizeFormat(format) if err != nil { return nil, err } cacheKey := s.cacheKeys.TextureRender(textureID, fmt.Sprintf("cape:%s", format), size) var cached RenderResult if err := s.cache.Get(ctx, cacheKey, &cached); err == nil && cached.URL != "" { return &cached, nil } texture, err := s.textureRepo.FindByID(ctx, textureID) if err != nil { return nil, fmt.Errorf("获取纹理失败: %w", err) } if texture == nil { return nil, errors.New("纹理不存在") } if texture.Type != model.TextureTypeCape { return nil, errors.New("仅披风纹理支持披风渲染") } textureData, srcInfo, err := s.downloadTexture(ctx, texture.URL) if err != nil { return nil, fmt.Errorf("下载纹理失败: %w", err) } img, err := png.Decode(bytes.NewReader(textureData)) if err != nil { return nil, fmt.Errorf("解码PNG失败: %w", err) } rendered := s.renderCapeView(img, size) encoded, err := encodeImage(rendered, format) if err != nil { return nil, fmt.Errorf("编码披风渲染失败: %w", err) } result, err := s.saveRenderToStorage(ctx, textureID, texture.Hash, RenderType("cape"), size, format, encoded, contentType) if err != nil { return nil, fmt.Errorf("保存披风渲染失败: %w", err) } if srcInfo != nil && result.LastModified.IsZero() { result.LastModified = srcInfo.LastModified } if err := s.cache.Set(ctx, cacheKey, result, time.Hour); err != nil { s.logger.Warn("缓存披风渲染失败", zap.Error(err)) } return result, nil } // RenderPreview 渲染预览图(类似 Blessing Skin preview) func (s *textureRenderService) RenderPreview(ctx context.Context, textureID int64, size int, format ImageFormat) (*RenderResult, error) { if size <= 0 || size > 2048 { return nil, errors.New("预览渲染尺寸必须在1到2048之间") } texture, err := s.textureRepo.FindByID(ctx, textureID) if err != nil { return nil, fmt.Errorf("获取纹理失败: %w", err) } if texture == nil { return nil, errors.New("纹理不存在") } switch texture.Type { case model.TextureTypeCape: return s.RenderCape(ctx, textureID, size, format) default: // 使用改进的等距视图作为默认预览 return s.RenderTexture(ctx, textureID, RenderTypeIsometric, size, format) } } // RenderTextureFromData 从原始数据渲染纹理 func (s *textureRenderService) RenderTextureFromData(ctx context.Context, textureData []byte, renderType RenderType, size int, format ImageFormat, isSlim bool) ([]byte, string, error) { // 解码PNG图像 img, err := png.Decode(bytes.NewReader(textureData)) if err != nil { return nil, "", fmt.Errorf("解码PNG失败: %w", err) } contentType, err := normalizeFormat(format) if err != nil { return nil, "", err } // 根据渲染类型处理图像 var renderedImage image.Image switch renderType { case RenderTypeFront: renderedImage = s.renderFrontView(img, isSlim, size) case RenderTypeBack: renderedImage = s.renderBackView(img, isSlim, size) case RenderTypeFull: renderedImage = s.renderFullView(img, isSlim, size) case RenderTypeHead: renderedImage = s.renderHeadView(img, size) case RenderTypeIsometric: renderedImage = s.renderIsometricView(img, isSlim, size) default: return nil, "", errors.New("不支持的渲染类型") } encoded, err := encodeImage(renderedImage, format) if err != nil { return nil, "", fmt.Errorf("编码纹理失败: %w", err) } return encoded, contentType, nil } // GetRenderURL 获取渲染图的URL func (s *textureRenderService) GetRenderURL(textureID int64, renderType RenderType, size int, format ImageFormat) string { // 构建渲染图的存储路径 // 格式: renders/{textureID}/{renderType}/{size}.{ext} ext := string(format) if ext == "" { ext = string(ImageFormatPNG) } return fmt.Sprintf("renders/%d/%s/%d.%s", textureID, renderType, size, ext) } // DeleteRenderCache 删除渲染缓存 func (s *textureRenderService) DeleteRenderCache(ctx context.Context, textureID int64) error { // 删除所有渲染类型与格式的缓存 renderTypes := []RenderType{ RenderTypeFront, RenderTypeBack, RenderTypeFull, RenderTypeHead, RenderTypeIsometric, RenderType("avatar-2d"), RenderType("avatar-3d"), RenderType("cape"), } formats := []ImageFormat{ImageFormatPNG, ImageFormatWEBP} sizes := []int{64, 128, 256, 512} for _, renderType := range renderTypes { for _, size := range sizes { for _, format := range formats { cacheKey := s.cacheKeys.TextureRender(textureID, fmt.Sprintf("%s:%s", renderType, format), size) if err := s.cache.Delete(ctx, cacheKey); err != nil { s.logger.Warn("删除渲染缓存失败", zap.Error(err)) } } } } return nil } // downloadTexture 从对象存储下载纹理 func (s *textureRenderService) downloadTexture(ctx context.Context, textureURL string) ([]byte, *storage.ObjectInfo, error) { // 先直接通过 HTTP GET 下载(对公有/匿名可读对象最兼容) if resp, httpErr := http.Get(textureURL); httpErr == nil && resp != nil && resp.StatusCode == http.StatusOK { defer resp.Body.Close() body, readErr := io.ReadAll(resp.Body) if readErr == nil { var lm time.Time if t, parseErr := http.ParseTime(resp.Header.Get("Last-Modified")); parseErr == nil { lm = t } return body, &storage.ObjectInfo{ Size: resp.ContentLength, LastModified: lm, ContentType: resp.Header.Get("Content-Type"), ETag: resp.Header.Get("ETag"), }, nil } } // 若 HTTP 失败,再尝试通过对象存储 SDK 访问 bucket, objectName, err := s.storage.ParseFileURL(textureURL) if err != nil { return nil, nil, fmt.Errorf("解析纹理URL失败: %w", err) } reader, info, err := s.storage.GetObject(ctx, bucket, objectName) if err != nil { s.logger.Error("获取纹理对象失败", zap.String("texture_url", textureURL), zap.String("bucket", bucket), zap.String("object", objectName), zap.Error(err), ) return nil, nil, fmt.Errorf("获取纹理对象失败: bucket=%s object=%s err=%v", bucket, objectName, err) } defer reader.Close() data, readErr := io.ReadAll(reader) if readErr != nil { return nil, nil, readErr } return data, info, nil } // saveRenderToStorage 保存渲染结果到对象存储 func (s *textureRenderService) saveRenderToStorage(ctx context.Context, textureID int64, textureHash string, renderType RenderType, size int, format ImageFormat, imageData []byte, contentType string) (*RenderResult, error) { // 获取存储桶 bucketName, err := s.storage.GetBucket("renders") if err != nil { // 如果renders桶不存在,使用textures桶 bucketName, err = s.storage.GetBucket("textures") if err != nil { return nil, fmt.Errorf("获取存储桶失败: %w", err) } } if len(textureHash) < 4 { return nil, errors.New("纹理哈希长度不足,无法生成路径") } ext := string(format) objectName := fmt.Sprintf("renders/%s/%s/%s_%s_%d.%s", textureHash[:2], textureHash[2:4], textureHash, renderType, size, ext) // 上传到对象存储 reader := bytes.NewReader(imageData) if err := s.storage.UploadObject(ctx, bucketName, objectName, reader, int64(len(imageData)), contentType); err != nil { return nil, fmt.Errorf("上传渲染结果失败: %w", err) } etag := sha256.Sum256(imageData) result := &RenderResult{ URL: s.storage.BuildFileURL(bucketName, objectName), ContentType: contentType, ETag: hex.EncodeToString(etag[:]), LastModified: time.Now().UTC(), Size: int64(len(imageData)), } return result, nil } // renderFrontView 渲染正面视图(分块+第二层,含 Alex/Steve) func (s *textureRenderService) renderFrontView(img image.Image, isSlim bool, size int) image.Image { base := composeFrontModel(img, isSlim) return scaleNearest(base, size, size) } // renderBackView 渲染背面视图(分块+第二层) func (s *textureRenderService) renderBackView(img image.Image, isSlim bool, size int) image.Image { base := composeBackModel(img, isSlim) return scaleNearest(base, size, size) } // renderFullView 渲染全身视图(正面+背面) func (s *textureRenderService) renderFullView(img image.Image, isSlim bool, size int) image.Image { front := composeFrontModel(img, isSlim) back := composeBackModel(img, isSlim) full := image.NewRGBA(image.Rect(0, 0, front.Bounds().Dx()+back.Bounds().Dx(), front.Bounds().Dy())) draw.Draw(full, image.Rect(0, 0, front.Bounds().Dx(), front.Bounds().Dy()), front, image.Point{}, draw.Src) draw.Draw(full, image.Rect(front.Bounds().Dx(), 0, full.Bounds().Dx(), full.Bounds().Dy()), back, image.Point{}, draw.Src) return scaleNearest(full, size*2, size) } // renderHeadView 渲染头像视图(包含第二层帽子) func (s *textureRenderService) renderHeadView(img image.Image, size int) image.Image { headBase := safeCrop(img, image.Rect(8, 8, 16, 16)) headOverlay := safeCrop(img, image.Rect(40, 8, 48, 16)) if headBase == nil { // 返回空白头像 return scaleNearest(image.NewRGBA(image.Rect(0, 0, 8, 8)), size, size) } canvas := image.NewRGBA(image.Rect(0, 0, headBase.Bounds().Dx(), headBase.Bounds().Dy())) draw.Draw(canvas, canvas.Bounds(), headBase, headBase.Bounds().Min, draw.Src) if headOverlay != nil { draw.Draw(canvas, canvas.Bounds(), headOverlay, headOverlay.Bounds().Min, draw.Over) } return scaleNearest(canvas, size, size) } // renderIsometricView 渲染等距视图(使用 Blessing Skin 风格的真 3D 渲染) func (s *textureRenderService) renderIsometricView(img image.Image, isSlim bool, size int) image.Image { // 将图像编码为 PNG 数据 var buf bytes.Buffer if err := png.Encode(&buf, img); err != nil { // 编码失败,回退到简单渲染 return s.renderIsometricViewFallback(img, isSlim, size) } // 使用新的 3D 渲染器渲染完整皮肤 ratio := float64(size) / 32.0 // 基准比例,32 像素高度的皮肤 rendered, err := s.minecraft.RenderSkin(buf.Bytes(), ratio, isSlim) if err != nil { // 渲染失败,回退到简单渲染 return s.renderIsometricViewFallback(img, isSlim, size) } return rendered } // renderIsometricViewFallback 等距视图回退方案(简单 2D) func (s *textureRenderService) renderIsometricViewFallback(img image.Image, isSlim bool, size int) image.Image { result := image.NewRGBA(image.Rect(0, 0, size, size)) bgColor := color.RGBA{240, 240, 240, 255} draw.Draw(result, result.Bounds(), &image.Uniform{bgColor}, image.Point{}, draw.Src) front := scaleNearest(composeFrontModel(img, isSlim), size/2, size/2) for y := 0; y < front.Bounds().Dy(); y++ { for x := 0; x < front.Bounds().Dx(); x++ { destX := x + size/4 destY := y + size/4 depth := float64(x) / float64(front.Bounds().Dx()) brightness := 1.0 - depth*0.25 c := front.At(x, y) r, g, b, a := c.RGBA() newR := uint32(float64(r) * brightness) newG := uint32(float64(g) * brightness) newB := uint32(float64(b) * brightness) if a > 0 { result.Set(destX, destY, color.RGBA64{ R: uint16(newR), G: uint16(newG), B: uint16(newB), A: uint16(a), }) } } } borderColor := color.RGBA{200, 200, 200, 255} for i := 0; i < 2; i++ { drawLine(result, size/4, size/4, size*3/4, size/4, borderColor) drawLine(result, size/4, size*3/4, size*3/4, size*3/4, borderColor) drawLine(result, size/4, size/4, size/4, size*3/4, borderColor) drawLine(result, size*3/4, size/4, size*3/4, size*3/4, borderColor) } return result } // drawLine 绘制直线 func drawLine(img *image.RGBA, x1, y1, x2, y2 int, c color.Color) { dx := abs(x2 - x1) dy := abs(y2 - y1) sx := -1 if x1 < x2 { sx = 1 } sy := -1 if y1 < y2 { sy = 1 } err := dx - dy for { img.Set(x1, y1, c) if x1 == x2 && y1 == y2 { break } e2 := 2 * err if e2 > -dy { err -= dy x1 += sx } if e2 < dx { err += dx y1 += sy } } } // abs 绝对值 func abs(x int) int { if x < 0 { return -x } return x } // renderCapeView 渲染披风(使用新渲染器) func (s *textureRenderService) renderCapeView(img image.Image, size int) image.Image { // 将图像编码为 PNG 数据 var buf bytes.Buffer if err := png.Encode(&buf, img); err != nil { // 编码失败,回退到简单缩放 srcBounds := img.Bounds() if srcBounds.Dx() == 0 || srcBounds.Dy() == 0 { return img } return scaleNearest(img, size*2, size) } // 使用新的披风渲染器 rendered, err := s.minecraft.RenderCape(buf.Bytes(), size) if err != nil { // 渲染失败,回退到简单缩放 srcBounds := img.Bounds() if srcBounds.Dx() == 0 || srcBounds.Dy() == 0 { return img } return scaleNearest(img, size*2, size) } return rendered } // composeFrontModel 组合正面分块(含第二层) func composeFrontModel(img image.Image, isSlim bool) *image.RGBA { canvas := image.NewRGBA(image.Rect(0, 0, 16, 32)) armW := 4 if isSlim { armW = 3 } drawLayeredPart(canvas, image.Rect(4, 0, 12, 8), safeCrop(img, image.Rect(8, 8, 16, 16)), safeCrop(img, image.Rect(40, 8, 48, 16))) drawLayeredPart(canvas, image.Rect(4, 8, 12, 20), safeCrop(img, image.Rect(20, 20, 28, 32)), safeCrop(img, image.Rect(20, 36, 28, 48))) drawLayeredPart(canvas, image.Rect(0, 8, armW, 20), safeCrop(img, image.Rect(44, 20, 48, 32)), safeCrop(img, image.Rect(44, 36, 48, 48))) drawLayeredPart(canvas, image.Rect(16-armW, 8, 16, 20), safeCrop(img, image.Rect(36, 52, 40, 64)), safeCrop(img, image.Rect(52, 52, 56, 64))) drawLayeredPart(canvas, image.Rect(4, 20, 8, 32), safeCrop(img, image.Rect(4, 20, 8, 32)), safeCrop(img, image.Rect(4, 36, 8, 48))) drawLayeredPart(canvas, image.Rect(8, 20, 12, 32), safeCrop(img, image.Rect(20, 52, 24, 64)), safeCrop(img, image.Rect(4, 52, 8, 64))) return canvas } // composeBackModel 组合背面分块(含第二层) func composeBackModel(img image.Image, isSlim bool) *image.RGBA { canvas := image.NewRGBA(image.Rect(0, 0, 16, 32)) armW := 4 if isSlim { armW = 3 } drawLayeredPart(canvas, image.Rect(4, 0, 12, 8), safeCrop(img, image.Rect(24, 8, 32, 16)), safeCrop(img, image.Rect(56, 8, 64, 16))) drawLayeredPart(canvas, image.Rect(4, 8, 12, 20), safeCrop(img, image.Rect(32, 20, 40, 32)), safeCrop(img, image.Rect(32, 36, 40, 48))) drawLayeredPart(canvas, image.Rect(0, 8, armW, 20), safeCrop(img, image.Rect(52, 20, 56, 32)), safeCrop(img, image.Rect(52, 36, 56, 48))) drawLayeredPart(canvas, image.Rect(16-armW, 8, 16, 20), safeCrop(img, image.Rect(44, 52, 48, 64)), safeCrop(img, image.Rect(60, 52, 64, 64))) drawLayeredPart(canvas, image.Rect(4, 20, 8, 32), safeCrop(img, image.Rect(12, 20, 16, 32)), safeCrop(img, image.Rect(12, 36, 16, 48))) drawLayeredPart(canvas, image.Rect(8, 20, 12, 32), safeCrop(img, image.Rect(28, 52, 32, 64)), safeCrop(img, image.Rect(12, 52, 16, 64))) return canvas } // drawLayeredPart 绘制单个分块(基础层+第二层,正确的 Alpha 混合) func drawLayeredPart(dst draw.Image, dstRect image.Rectangle, base image.Image, overlay image.Image) { if base == nil { return } dstW := dstRect.Dx() dstH := dstRect.Dy() // 绘制基础层 for y := 0; y < dstH; y++ { for x := 0; x < dstW; x++ { srcX := base.Bounds().Min.X + x*base.Bounds().Dx()/dstW srcY := base.Bounds().Min.Y + y*base.Bounds().Dy()/dstH dst.Set(dstRect.Min.X+x, dstRect.Min.Y+y, base.At(srcX, srcY)) } } // 绘制第二层(使用 Alpha 混合) if overlay != nil { for y := 0; y < dstH; y++ { for x := 0; x < dstW; x++ { srcX := overlay.Bounds().Min.X + x*overlay.Bounds().Dx()/dstW srcY := overlay.Bounds().Min.Y + y*overlay.Bounds().Dy()/dstH overlayColor := overlay.At(srcX, srcY) // 获取 overlay 的 alpha 值 _, _, _, a := overlayColor.RGBA() if a == 0 { // 完全透明,跳过 continue } if a == 0xFFFF { // 完全不透明,直接覆盖 dst.Set(dstRect.Min.X+x, dstRect.Min.Y+y, overlayColor) } else { // 半透明,进行 Alpha 混合 baseColor := dst.At(dstRect.Min.X+x, dstRect.Min.Y+y) blended := alphaBlendColors(baseColor, overlayColor) dst.Set(dstRect.Min.X+x, dstRect.Min.Y+y, blended) } } } } } // alphaBlendColors 执行 Alpha 混合 func alphaBlendColors(dst, src color.Color) color.Color { sr, sg, sb, sa := src.RGBA() dr, dg, db, da := dst.RGBA() if sa == 0 { return dst } if sa == 0xFFFF { return src } // Alpha 混合公式 srcA := float64(sa) / 0xFFFF dstA := float64(da) / 0xFFFF outA := srcA + dstA*(1-srcA) if outA == 0 { return color.RGBA{} } outR := (float64(sr)*srcA + float64(dr)*dstA*(1-srcA)) / outA outG := (float64(sg)*srcA + float64(dg)*dstA*(1-srcA)) / outA outB := (float64(sb)*srcA + float64(db)*dstA*(1-srcA)) / outA return color.RGBA64{ R: uint16(outR), G: uint16(outG), B: uint16(outB), A: uint16(outA * 0xFFFF), } } // safeCrop 安全裁剪(超界返回nil) func safeCrop(img image.Image, rect image.Rectangle) image.Image { b := img.Bounds() if rect.Min.X < 0 || rect.Min.Y < 0 || rect.Max.X > b.Max.X || rect.Max.Y > b.Max.Y { return nil } subImg := image.NewRGBA(rect) draw.Draw(subImg, rect, img, rect.Min, draw.Src) return subImg } // scaleNearest 最近邻缩放 func scaleNearest(src image.Image, targetW, targetH int) *image.RGBA { dst := image.NewRGBA(image.Rect(0, 0, targetW, targetH)) srcBounds := src.Bounds() for y := 0; y < targetH; y++ { for x := 0; x < targetW; x++ { srcX := srcBounds.Min.X + x*srcBounds.Dx()/targetW srcY := srcBounds.Min.Y + y*srcBounds.Dy()/targetH dst.Set(x, y, src.At(srcX, srcY)) } } return dst } // normalizeFormat 校验输出格式 func normalizeFormat(format ImageFormat) (string, error) { if format == "" { format = ImageFormatPNG } switch format { case ImageFormatPNG: return "image/png", nil case ImageFormatWEBP: return "image/webp", nil default: return "", fmt.Errorf("不支持的输出格式: %s", format) } } // encodeImage 将图像编码为指定格式 func encodeImage(img image.Image, format ImageFormat) ([]byte, error) { var buf bytes.Buffer switch format { case ImageFormatWEBP: if err := webp.Encode(&buf, img, &webp.Options{Lossless: true}); err != nil { return nil, err } default: if err := png.Encode(&buf, img); err != nil { return nil, err } } return buf.Bytes(), nil }