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