commit 615b27dd414c0e88eb58b42f095574cbf8d22fd0 Author: dev Date: Mon Jan 26 21:43:58 2026 +0100 First diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/ImgResizer.iml b/.idea/ImgResizer.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/ImgResizer.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..29782a9 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/ImgResizer.exe b/ImgResizer.exe new file mode 100644 index 0000000..9dff3b0 Binary files /dev/null and b/ImgResizer.exe differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8d32db3 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module ImgResizer + +go 1.25 + +require ( + github.com/chai2010/webp v1.4.0 + github.com/disintegration/imaging v1.6.2 +) + +require golang.org/x/image v0.35.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2c6c051 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko= +github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I= +golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/imgs/aseo_alpino_ducha_salvia.jpg b/imgs/aseo_alpino_ducha_salvia.jpg new file mode 100644 index 0000000..36eac78 Binary files /dev/null and b/imgs/aseo_alpino_ducha_salvia.jpg differ diff --git a/imgs/aseo_alpino_ducha_trentino.jpg b/imgs/aseo_alpino_ducha_trentino.jpg new file mode 100644 index 0000000..04de904 Binary files /dev/null and b/imgs/aseo_alpino_ducha_trentino.jpg differ diff --git a/imgs/aseo_alpino_ducha_zucchero.jpg b/imgs/aseo_alpino_ducha_zucchero.jpg new file mode 100644 index 0000000..ccc99c9 Binary files /dev/null and b/imgs/aseo_alpino_ducha_zucchero.jpg differ diff --git a/imgsout/aseo_alpino_ducha_salvia.jpg b/imgsout/aseo_alpino_ducha_salvia.jpg new file mode 100644 index 0000000..889750e Binary files /dev/null and b/imgsout/aseo_alpino_ducha_salvia.jpg differ diff --git a/imgsout/aseo_alpino_ducha_trentino.jpg b/imgsout/aseo_alpino_ducha_trentino.jpg new file mode 100644 index 0000000..5757ba7 Binary files /dev/null and b/imgsout/aseo_alpino_ducha_trentino.jpg differ diff --git a/imgsout/aseo_alpino_ducha_zucchero.jpg b/imgsout/aseo_alpino_ducha_zucchero.jpg new file mode 100644 index 0000000..644d7ae Binary files /dev/null and b/imgsout/aseo_alpino_ducha_zucchero.jpg differ diff --git a/main.go b/main.go new file mode 100644 index 0000000..5322bc8 --- /dev/null +++ b/main.go @@ -0,0 +1,399 @@ +/* + —————————————————————————————————————————————————————————————————————————— + Proyecto ImgResizer + +————————————————————————————————————————————————————————————————————————————— + + Fichero main.go + 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 -out [-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") + } +} diff --git a/main.go1 b/main.go1 new file mode 100644 index 0000000..ea83ab5 --- /dev/null +++ b/main.go1 @@ -0,0 +1,151 @@ +/* —————————————————————————————————————————————————————————————————————————— + Proyecto ImgResizer +————————————————————————————————————————————————————————————————————————————— + Fichero main.go + Package ImgResizer + Autor Juan V. Navarro juanvnl@activething.com + Creado 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" + + "github.com/disintegration/imaging" + // Decodificador Pure Go para WebP (No requiere CGO) + _ "golang.org/x/image/webp" +) + +func main() { + inputDir := flag.String("dir", "", "Directorio de origen") + outputDir := flag.String("out", "", "Directorio de destino") + width := flag.Int("width", 0, "Ancho (excluyente)") + height := flag.Int("height", 0, "Alto (excluyente)") + quality := flag.Int("calidad", 100, "Calidad (1-100)") + + configurarAyuda() + flag.Parse() + + if *inputDir == "" || *outputDir == "" || (*width == 0 && *height == 0) { + flag.Usage() + return + } + + if *width > 0 && *height > 0 { + log.Fatal("Error: -width y -height son excluyentes.") + } + + os.MkdirAll(*outputDir, 0755) + + files, _ := os.ReadDir(*inputDir) + + for _, file := range files { + ext := strings.ToLower(filepath.Ext(file.Name())) + if ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".webp" { + continue + } + + fmt.Printf("Procesando %s... ", file.Name()) + err := processImage(filepath.Join(*inputDir, file.Name()), filepath.Join(*outputDir, file.Name()), *width, *height, *quality) + + if err != nil { + fmt.Printf("ERROR: %v\n", err) + } else { + fmt.Println("OK") + } + } +} + +func processImage(src, dst string, w, h, q int) error { + // 1. Abrir imagen (Soporta WebP, JPG, PNG gracias a los imports con _) + f, err := os.Open(src) + if err != nil { + return err + } + defer f.Close() + + img, _, err := image.Decode(f) + if err != nil { + return err + } + + // 2. Redimensionar + resized := imaging.Resize(img, w, h, imaging.Lanczos) + + // 3. Guardar según extensión + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + ext := strings.ToLower(filepath.Ext(dst)) + switch ext { + case ".jpg", ".jpeg": + return jpeg.Encode(out, resized, &jpeg.Options{Quality: q}) + case ".png": + // El encoder estándar de Go es muy robusto + return png.Encode(out, resized) + case ".webp": + // Si llegas aquí sin CGO, el programa dirá que no sabe codificar WebP + // pero al menos el resto del programa COMPILARÁ y FUNCIONARÁ + return fmt.Errorf("la escritura de WebP requiere CGO y un compilador de C (GCC)") + default: + return imaging.Save(resized, dst) + } +} + +func configurarAyuda() { + flag.Usage = func() { + fmt.Printf("\nUso: resizer -dir -out [-width | -height]\n") + flag.PrintDefaults() + } +}