Files
backend/internal/service/texture_render_service.go
lan a51535a465 feat: Add texture rendering endpoints and service methods
- Introduced new API endpoints for rendering textures, avatars, capes, and previews, enhancing the texture handling capabilities.
- Implemented corresponding service methods in the TextureHandler to process rendering requests and return appropriate responses.
- Updated the TextureRenderService interface to include methods for rendering textures, avatars, and capes, along with their respective parameters.
- Enhanced error handling for invalid texture IDs and added support for different rendering types and formats.
- Updated go.mod to include the webp library for image processing.
2025-12-07 10:10:28 +08:00

702 lines
21 KiB
Go
Raw 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 service
import (
"bytes"
"carrotskin/internal/model"
"carrotskin/internal/repository"
"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
}
// 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,
}
}
// 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)
}
img, err := png.Decode(bytes.NewReader(textureData))
if err != nil {
return nil, fmt.Errorf("解码PNG失败: %w", err)
}
var rendered image.Image
switch mode {
case AvatarMode3D:
rendered = s.renderIsometricView(img, texture.IsSlim, size)
default:
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 渲染等距视图(改进 3D
func (s *textureRenderService) renderIsometricView(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 {
srcBounds := img.Bounds()
if srcBounds.Dx() == 0 || srcBounds.Dy() == 0 {
return img
}
targetWidth := size * 2
targetHeight := size
return scaleNearest(img, targetWidth, targetHeight)
}
// 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 绘制单个分块(基础层+第二层)
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))
}
}
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
dst.Set(dstRect.Min.X+x, dstRect.Min.Y+y, overlay.At(srcX, srcY))
}
}
}
}
// 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
}