parent
615b27dd41
commit
1275b6cc50
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
@ -0,0 +1,399 @@
|
||||
/*
|
||||
——————————————————————————————————————————————————————————————————————————
|
||||
Proyecto ImgResizer
|
||||
|
||||
—————————————————————————————————————————————————————————————————————————————
|
||||
|
||||
Fichero main.go2
|
||||
Package ImgResizer
|
||||
Autor Juan V. Navarro juanvnl@activething.com
|
||||
Creado 26/01/2026
|
||||
Mejorado 26/01/2026
|
||||
|
||||
—————————————————————————————————————————————————————————————————————————————
|
||||
|
||||
LICENSES AND TERMS OF USE
|
||||
-------------------------
|
||||
|
||||
This software is licensed under the Elastic License v2.0 (the "License").
|
||||
For full terms and additional information regarding permitted and prohibited
|
||||
uses, please visit:
|
||||
https://activething.com/ATGO/licenses
|
||||
|
||||
You may use, copy, modify, and redistribute this software internally within
|
||||
your organization for any purpose, including research, development, and
|
||||
testing, subject to the terms of this License.
|
||||
|
||||
You may NOT, however, use, provide, distribute, or make this software
|
||||
available to any third party as part of a hosted service, SaaS offering, or
|
||||
commercial product without first obtaining a commercial license from
|
||||
Active Thing.
|
||||
|
||||
You may combine this software with other code, provided that such
|
||||
combination does not circumvent the restrictions of this License.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. In no event shall
|
||||
the authors or copyright holders be liable for any claim, damages, or other
|
||||
liability arising from the use of this software.
|
||||
|
||||
—————————————————————————————————————————————————————————————————————————————
|
||||
|
||||
Web : activething.com | activething.com/ImgResizer
|
||||
git : g.activething.com | github.com/activething/ImgResizer
|
||||
Correo : dev@activething.com
|
||||
|
||||
—————————————————————————————————————————————————————————————————————————————
|
||||
|
||||
No deseo caminar sobre el agua", dijo Siddhartha.
|
||||
Que los antiguos chamanes se contenten con tales habilidades.
|
||||
—— Hermann Hesse, Siddhartha
|
||||
|
||||
—————————————————————————————————————————————————————————————————————————————
|
||||
|
||||
Copyright (c) 2026 Active Thing
|
||||
|
||||
—————————————————————————————————————————————————————————————————————————————
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
InputDir string
|
||||
OutputDir string
|
||||
Width int
|
||||
Height int
|
||||
Quality int
|
||||
Format string
|
||||
Filter string
|
||||
Workers int
|
||||
Verbose bool
|
||||
KeepStructure bool
|
||||
}
|
||||
|
||||
type ProcessResult struct {
|
||||
Filename string
|
||||
Success bool
|
||||
Error error
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := parseFlags()
|
||||
|
||||
if err := validateConfig(cfg); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(cfg.OutputDir, 0755); err != nil {
|
||||
log.Fatalf("Error creando directorio de salida: %v", err)
|
||||
}
|
||||
|
||||
files, err := collectImageFiles(cfg.InputDir, cfg.KeepStructure)
|
||||
if err != nil {
|
||||
log.Fatalf("Error leyendo directorio: %v", err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
log.Println("No se encontraron imágenes para procesar")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Encontradas %d imágenes. Procesando con %d workers...\n\n", len(files), cfg.Workers)
|
||||
|
||||
processImages(files, cfg)
|
||||
}
|
||||
|
||||
func parseFlags() *Config {
|
||||
cfg := &Config{}
|
||||
|
||||
flag.StringVar(&cfg.InputDir, "dir", "", "Directorio de origen")
|
||||
flag.StringVar(&cfg.OutputDir, "out", "", "Directorio de destino")
|
||||
flag.IntVar(&cfg.Width, "width", 0, "Ancho objetivo (mantiene proporción si height=0)")
|
||||
flag.IntVar(&cfg.Height, "height", 0, "Alto objetivo (mantiene proporción si width=0)")
|
||||
flag.IntVar(&cfg.Quality, "calidad", 100, "Calidad JPEG (1-100)")
|
||||
flag.StringVar(&cfg.Format, "formato", "", "Formato de salida: jpg, png (vacío=mantener original)")
|
||||
flag.StringVar(&cfg.Filter, "filtro", "lanczos", "Filtro de redimensionado: lanczos, cubic, linear, box")
|
||||
flag.IntVar(&cfg.Workers, "workers", 4, "Número de workers concurrentes")
|
||||
flag.BoolVar(&cfg.Verbose, "verbose", false, "Mostrar información detallada")
|
||||
flag.BoolVar(&cfg.KeepStructure, "estructura", false, "Mantener estructura de subdirectorios")
|
||||
|
||||
configurarAyuda()
|
||||
flag.Parse()
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func validateConfig(cfg *Config) error {
|
||||
if cfg.InputDir == "" || cfg.OutputDir == "" {
|
||||
flag.Usage()
|
||||
return fmt.Errorf("faltan parámetros obligatorios")
|
||||
}
|
||||
|
||||
if cfg.Width == 0 && cfg.Height == 0 {
|
||||
return fmt.Errorf("debe especificar al menos -width o -height")
|
||||
}
|
||||
|
||||
if cfg.Width < 0 || cfg.Height < 0 {
|
||||
return fmt.Errorf("las dimensiones no pueden ser negativas")
|
||||
}
|
||||
|
||||
if cfg.Quality < 1 || cfg.Quality > 100 {
|
||||
return fmt.Errorf("la calidad debe estar entre 1 y 100")
|
||||
}
|
||||
|
||||
if cfg.Format != "" && cfg.Format != "jpg" && cfg.Format != "jpeg" && cfg.Format != "png" {
|
||||
return fmt.Errorf("formato no soportado: %s (use jpg o png)", cfg.Format)
|
||||
}
|
||||
|
||||
absInput, err := filepath.Abs(cfg.InputDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error resolviendo ruta de entrada: %v", err)
|
||||
}
|
||||
|
||||
absOutput, err := filepath.Abs(cfg.OutputDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error resolviendo ruta de salida: %v", err)
|
||||
}
|
||||
|
||||
if absInput == absOutput {
|
||||
return fmt.Errorf("el directorio de entrada y salida no pueden ser el mismo")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(cfg.InputDir); os.IsNotExist(err) {
|
||||
return fmt.Errorf("el directorio de entrada no existe: %s", cfg.InputDir)
|
||||
}
|
||||
|
||||
if cfg.Workers < 1 {
|
||||
cfg.Workers = 1
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func collectImageFiles(root string, keepStructure bool) ([]string, error) {
|
||||
var files []string
|
||||
validExts := map[string]bool{
|
||||
".jpg": true,
|
||||
".jpeg": true,
|
||||
".png": true,
|
||||
".webp": true,
|
||||
}
|
||||
|
||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
if validExts[ext] {
|
||||
files = append(files, path)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return files, err
|
||||
}
|
||||
|
||||
func processImages(files []string, cfg *Config) {
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
processed atomic.Int32
|
||||
errors atomic.Int32
|
||||
total = int32(len(files))
|
||||
)
|
||||
|
||||
jobs := make(chan string, len(files))
|
||||
results := make(chan ProcessResult, len(files))
|
||||
|
||||
// Iniciar workers
|
||||
for i := 0; i < cfg.Workers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for inputPath := range jobs {
|
||||
result := processImage(inputPath, cfg)
|
||||
results <- result
|
||||
|
||||
current := processed.Add(1)
|
||||
if result.Success {
|
||||
if cfg.Verbose {
|
||||
fmt.Printf("[%d/%d] ✓ %s\n", current, total, result.Filename)
|
||||
}
|
||||
} else {
|
||||
errors.Add(1)
|
||||
fmt.Printf("[%d/%d] ✗ %s: %v\n", current, total, result.Filename, result.Error)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Enviar trabajos
|
||||
for _, file := range files {
|
||||
jobs <- file
|
||||
}
|
||||
close(jobs)
|
||||
|
||||
// Esperar completar
|
||||
wg.Wait()
|
||||
close(results)
|
||||
|
||||
// Resumen final
|
||||
fmt.Printf("\n═══════════════════════════════════════════════════════\n")
|
||||
fmt.Printf("Procesamiento completado:\n")
|
||||
fmt.Printf(" Total: %d imágenes\n", total)
|
||||
fmt.Printf(" Exitosas: %d\n", total-errors.Load())
|
||||
fmt.Printf(" Errores: %d\n", errors.Load())
|
||||
fmt.Printf("═══════════════════════════════════════════════════════\n")
|
||||
}
|
||||
|
||||
func processImage(inputPath string, cfg *Config) ProcessResult {
|
||||
result := ProcessResult{
|
||||
Filename: filepath.Base(inputPath),
|
||||
Success: false,
|
||||
}
|
||||
|
||||
// Calcular ruta de salida
|
||||
relPath, err := filepath.Rel(cfg.InputDir, inputPath)
|
||||
if err != nil {
|
||||
result.Error = fmt.Errorf("error calculando ruta relativa: %v", err)
|
||||
return result
|
||||
}
|
||||
|
||||
outputPath := filepath.Join(cfg.OutputDir, relPath)
|
||||
|
||||
// Cambiar extensión si se especificó formato
|
||||
if cfg.Format != "" {
|
||||
ext := filepath.Ext(outputPath)
|
||||
outputPath = outputPath[:len(outputPath)-len(ext)] + "." + cfg.Format
|
||||
}
|
||||
|
||||
// Crear subdirectorios si es necesario
|
||||
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
|
||||
result.Error = fmt.Errorf("error creando directorio: %v", err)
|
||||
return result
|
||||
}
|
||||
|
||||
// Abrir imagen
|
||||
f, err := os.Open(inputPath)
|
||||
if err != nil {
|
||||
result.Error = fmt.Errorf("error abriendo archivo: %v", err)
|
||||
return result
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
img, format, err := image.Decode(f)
|
||||
if err != nil {
|
||||
result.Error = fmt.Errorf("error decodificando imagen: %v", err)
|
||||
return result
|
||||
}
|
||||
|
||||
if cfg.Verbose {
|
||||
bounds := img.Bounds()
|
||||
log.Printf(" Formato original: %s, Dimensiones: %dx%d", format, bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
|
||||
// Redimensionar
|
||||
filter := getResampleFilter(cfg.Filter)
|
||||
resized := imaging.Resize(img, cfg.Width, cfg.Height, filter)
|
||||
|
||||
// Guardar
|
||||
out, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
result.Error = fmt.Errorf("error creando archivo de salida: %v", err)
|
||||
return result
|
||||
}
|
||||
defer func() {
|
||||
if cerr := out.Close(); cerr != nil && result.Error == nil {
|
||||
result.Error = cerr
|
||||
}
|
||||
}()
|
||||
|
||||
// Determinar formato de salida
|
||||
outputFormat := cfg.Format
|
||||
if outputFormat == "" {
|
||||
outputFormat = strings.ToLower(filepath.Ext(outputPath)[1:])
|
||||
}
|
||||
|
||||
switch outputFormat {
|
||||
case "jpg", "jpeg":
|
||||
err = jpeg.Encode(out, resized, &jpeg.Options{Quality: cfg.Quality})
|
||||
case "png":
|
||||
err = png.Encode(out, resized)
|
||||
case "webp":
|
||||
err = fmt.Errorf("escritura de WebP requiere CGO (use -formato jpg o png para convertir)")
|
||||
default:
|
||||
err = imaging.Save(resized, outputPath)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
result.Error = fmt.Errorf("error guardando imagen: %v", err)
|
||||
return result
|
||||
}
|
||||
|
||||
result.Success = true
|
||||
return result
|
||||
}
|
||||
|
||||
func getResampleFilter(name string) imaging.ResampleFilter {
|
||||
switch strings.ToLower(name) {
|
||||
case "lanczos":
|
||||
return imaging.Lanczos
|
||||
case "cubic":
|
||||
return imaging.CatmullRom
|
||||
case "linear":
|
||||
return imaging.Linear
|
||||
case "box":
|
||||
return imaging.Box
|
||||
default:
|
||||
return imaging.Lanczos
|
||||
}
|
||||
}
|
||||
|
||||
func configurarAyuda() {
|
||||
flag.Usage = func() {
|
||||
fmt.Printf("\n╔═══════════════════════════════════════════════════════════════╗\n")
|
||||
fmt.Printf("║ ImgResizer - Redimensionador de Imágenes ║\n")
|
||||
fmt.Printf("╚═══════════════════════════════════════════════════════════════╝\n\n")
|
||||
fmt.Printf("Uso: resizer -dir <origen> -out <destino> [-width | -height] [opciones]\n\n")
|
||||
fmt.Printf("Parámetros obligatorios:\n")
|
||||
fmt.Printf(" -dir string\n")
|
||||
fmt.Printf(" Directorio de origen con las imágenes\n")
|
||||
fmt.Printf(" -out string\n")
|
||||
fmt.Printf(" Directorio de destino para imágenes procesadas\n")
|
||||
fmt.Printf(" -width int\n")
|
||||
fmt.Printf(" Ancho objetivo (mantiene proporción si height no se especifica)\n")
|
||||
fmt.Printf(" -height int\n")
|
||||
fmt.Printf(" Alto objetivo (mantiene proporción si width no se especifica)\n\n")
|
||||
fmt.Printf("Opciones:\n")
|
||||
flag.PrintDefaults()
|
||||
fmt.Printf("\nEjemplos:\n")
|
||||
fmt.Printf(" # Redimensionar a 800px de ancho manteniendo proporción\n")
|
||||
fmt.Printf(" resizer -dir ./input -out ./output -width 800\n\n")
|
||||
fmt.Printf(" # Convertir todas las imágenes a JPG de 1920x1080\n")
|
||||
fmt.Printf(" resizer -dir ./input -out ./output -width 1920 -height 1080 -formato jpg\n\n")
|
||||
fmt.Printf(" # Procesar con 8 workers y mantener estructura de carpetas\n")
|
||||
fmt.Printf(" resizer -dir ./input -out ./output -width 1024 -workers 8 -estructura\n\n")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue