feat: file_save_disk, file_delete, file_serve, upload_parse, upload_handler, thumbnail_generate (issue 0014 fase 3)
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user