master
dev 8 months ago
commit 615b27dd41

8
.idea/.gitignore vendored

@ -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

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ImgResizer.iml" filepath="$PROJECT_DIR$/.idea/ImgResizer.iml" />
</modules>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

Binary file not shown.

@ -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

@ -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=

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

@ -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 <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")
}
}

@ -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 <origen> -out <destino> [-width | -height]\n")
flag.PrintDefaults()
}
}
Loading…
Cancel
Save

Powered by TurnKey Linux.