package infra import ( "fmt" "image" "image/jpeg" "image/png" "os" "path/filepath" "strings" ) // ThumbnailGenerate lee una imagen JPEG o PNG desde srcPath, la redimensiona // manteniendo aspect ratio para que quepa dentro de (maxWidth, maxHeight) y guarda // el resultado en dstPath con el formato inferido por la extension de dstPath. // // Solo soporta entrada/salida JPEG y PNG (image stdlib). Formatos modernos como // WebP, AVIF o HEIC no estan soportados — retornan error explicito. // // El algoritmo de resize es nearest-neighbor (sin filtro) para mantener cero // dependencias externas. Para apps que necesiten calidad alta usar una libreria // como `golang.org/x/image/draw` con BiLinear o CatmullRom. func ThumbnailGenerate(srcPath string, dstPath string, maxWidth int, maxHeight int) error { if maxWidth <= 0 || maxHeight <= 0 { return fmt.Errorf("thumbnail_generate: maxWidth y maxHeight deben ser > 0") } srcF, err := os.Open(srcPath) if err != nil { return fmt.Errorf("thumbnail_generate: open src %s: %w", srcPath, err) } defer srcF.Close() srcImg, _, err := image.Decode(srcF) if err != nil { return fmt.Errorf("thumbnail_generate: decode %s: %w", srcPath, err) } thumb := resizeNearest(srcImg, maxWidth, maxHeight) if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil { return fmt.Errorf("thumbnail_generate: mkdir dst: %w", err) } dstF, err := os.Create(dstPath) if err != nil { return fmt.Errorf("thumbnail_generate: create dst %s: %w", dstPath, err) } defer dstF.Close() ext := strings.ToLower(filepath.Ext(dstPath)) switch ext { case ".jpg", ".jpeg": return jpeg.Encode(dstF, thumb, &jpeg.Options{Quality: 85}) case ".png": return png.Encode(dstF, thumb) default: return fmt.Errorf("thumbnail_generate: extension %q no soportada (solo jpg/jpeg/png)", ext) } } // resizeNearest redimensiona la imagen al maximo (maxW, maxH) manteniendo aspect // ratio. Usa interpolacion nearest-neighbor (rapida pero baja calidad). func resizeNearest(src image.Image, maxW, maxH int) image.Image { srcBounds := src.Bounds() srcW := srcBounds.Dx() srcH := srcBounds.Dy() if srcW == 0 || srcH == 0 { return src } // Calcular escala manteniendo aspect ratio scaleW := float64(maxW) / float64(srcW) scaleH := float64(maxH) / float64(srcH) scale := scaleW if scaleH < scaleW { scale = scaleH } if scale >= 1.0 { // Imagen ya cabe, no agrandar return src } dstW := int(float64(srcW) * scale) dstH := int(float64(srcH) * scale) if dstW < 1 { dstW = 1 } if dstH < 1 { dstH = 1 } dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH)) for y := 0; y < dstH; y++ { srcY := int(float64(y) / scale) for x := 0; x < dstW; x++ { srcX := int(float64(x) / scale) dst.Set(x, y, src.At(srcBounds.Min.X+srcX, srcBounds.Min.Y+srcY)) } } return dst }