You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

790 lines
29 KiB

8 months ago
/*
7 months ago
——————————————————————————————————————————————————————————————————————————
Proyecto Imaginator
8 months ago
7 months ago
—————————————————————————————————————————————————————————————————————————
8 months ago
Fichero main.go
7 months ago
Package Imaginator
8 months ago
Autor Juan V. Navarro juanvnl@activething.com
Creado 26/01/2026
7 months ago
Mejorado 20/03/2026
8 months ago
7 months ago
—————————————————————————————————————————————————————————————————————————
8 months ago
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.
7 months ago
—————————————————————————————————————————————————————————————————————————
8 months ago
7 months ago
Web : activething.com | activething.com/Imaginator
git : g.activething.com | github.com/activething/Imaginator
8 months ago
Correo : dev@activething.com
7 months ago
—————————————————————————————————————————————————————————————————————————
8 months ago
7 months ago
"No deseo caminar sobre el agua", dijo Siddhartha.
8 months ago
Que los antiguos chamanes se contenten con tales habilidades.
—— Hermann Hesse, Siddhartha
7 months ago
—————————————————————————————————————————————————————————————————————————
8 months ago
Copyright (c) 2026 Active Thing
7 months ago
—————————————————————————————————————————————————————————————————————————
8 months ago
*/
package main
import (
"flag"
"fmt"
"image"
"image/jpeg"
"image/png"
"log"
"os"
"path/filepath"
7 months ago
"regexp"
8 months ago
"strings"
"sync"
"sync/atomic"
7 months ago
"time"
"unicode"
8 months ago
"github.com/disintegration/imaging"
7 months ago
_ "golang.org/x/image/bmp"
"golang.org/x/image/tiff"
8 months ago
_ "golang.org/x/image/webp"
)
7 months ago
const version = "2.3.0"
// ─────────────────────────────────────────────────────────────────────────────
// Tipos
// ─────────────────────────────────────────────────────────────────────────────
8 months ago
type Config struct {
InputDir string
OutputDir string
Width int
Height int
Quality int
Format string
Filter string
Workers int
Verbose bool
KeepStructure bool
7 months ago
Prefix string
Suffix string
DryRun bool
Replace string // pares orig/dest separados por coma, case sensitive
ReplaceI string // ídem, case insensitive
ReplaceP string // ídem, case insensitive + preserve case
FileFilter string // glob sobre el nombre del fichero
Lowercase bool // normalizar nombre de salida a minúsculas
Uppercase bool // normalizar nombre de salida a mayúsculas
}
// ReplacePair representa un par de sustitución ya parseado
type ReplacePair struct {
From string
To string
8 months ago
}
type ProcessResult struct {
7 months ago
Filename string
OutputPath string
Success bool
Error error
Duration time.Duration
InputSize int64
OutputSize int64
}
type ImageSummary struct {
Files []string
ByExt map[string]int
ReadOnlyExts []string // extensiones presentes que son sólo lectura
8 months ago
}
7 months ago
// ─────────────────────────────────────────────────────────────────────────────
// Entrada
// ─────────────────────────────────────────────────────────────────────────────
8 months ago
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)
}
7 months ago
summary, err := collectImageFiles(cfg.InputDir, cfg.FileFilter)
8 months ago
if err != nil {
log.Fatalf("Error leyendo directorio: %v", err)
}
7 months ago
if len(summary.Files) == 0 {
fmt.Println(" No se encontraron imágenes para procesar.")
8 months ago
return
}
7 months ago
// Resumen de tipos encontrados
fmt.Printf(" Encontradas %d imágenes:\n", len(summary.Files))
for _, ext := range []string{".jpg", ".jpeg", ".png", ".tif", ".tiff", ".bmp", ".webp"} {
if n := summary.ByExt[ext]; n > 0 {
line := fmt.Sprintf(" %-8s %d", ext, n)
if (ext == ".webp" || ext == ".bmp") && cfg.Format == "" {
line += " ⚠ sólo lectura"
}
fmt.Println(line)
}
}
if len(summary.ReadOnlyExts) > 0 && cfg.Format == "" {
joined := strings.Join(summary.ReadOnlyExts, ", ")
fmt.Println()
fmt.Printf(" AVISO: el lote contiene formatos sólo lectura (%s).\n", joined)
fmt.Println(" Los ficheros de salida en ese formato pueden quedar corruptos.")
fmt.Println(" Añade -formato jpg o -formato png para convertirlos.")
}
fmt.Println()
if cfg.DryRun {
fmt.Println(" [dry-run] Rutas de salida:")
fmt.Println()
for _, f := range summary.Files {
out := buildOutputPath(f, cfg)
fmt.Printf(" %s → %s\n", f, out)
}
fmt.Println()
return
}
8 months ago
7 months ago
fmt.Printf(" Procesando con %d workers...\n\n", cfg.Workers)
processImages(summary.Files, cfg)
8 months ago
}
7 months ago
// ─────────────────────────────────────────────────────────────────────────────
// Flags y validación
// ─────────────────────────────────────────────────────────────────────────────
8 months ago
func parseFlags() *Config {
cfg := &Config{}
flag.StringVar(&cfg.InputDir, "dir", "", "Directorio de origen")
flag.StringVar(&cfg.OutputDir, "out", "", "Directorio de destino")
7 months ago
flag.IntVar(&cfg.Width, "width", 0, "Ancho objetivo")
flag.IntVar(&cfg.Height, "height", 0, "Alto objetivo")
flag.IntVar(&cfg.Quality, "calidad", 85, "Calidad JPEG (1-100)")
flag.StringVar(&cfg.Format, "formato", "", "Formato de salida: jpg, png, tif")
flag.StringVar(&cfg.Filter, "filtro", "lanczos", "Filtro: lanczos, cubic, linear, box")
flag.IntVar(&cfg.Workers, "workers", 4, "Workers concurrentes")
flag.BoolVar(&cfg.Verbose, "verbose", false, "Información detallada")
8 months ago
flag.BoolVar(&cfg.KeepStructure, "estructura", false, "Mantener estructura de subdirectorios")
7 months ago
flag.StringVar(&cfg.Prefix, "prefijo", "", "Prefijo para el nombre de salida")
flag.StringVar(&cfg.Suffix, "sufijo", "", "Sufijo para el nombre de salida (antes de la extensión)")
flag.BoolVar(&cfg.DryRun, "dry-run", false, "Mostrar qué se procesaría sin ejecutar nada")
flag.StringVar(&cfg.Replace, "replace", "", "Sustituir tokens en nombre: orig/dest[,orig2/dest2] (case sensitive)")
flag.StringVar(&cfg.ReplaceI, "replacei", "", "Sustituir tokens en nombre: orig/dest[,...] (case insensitive)")
flag.StringVar(&cfg.ReplaceP, "replacep", "", "Sustituir tokens en nombre: orig/dest[,...] (insensitive + preserve case)")
flag.StringVar(&cfg.FileFilter, "filter", "", "Glob de nombre de fichero a procesar, ej: foto_*.jpg")
flag.BoolVar(&cfg.Lowercase, "lowercase", false, "Normalizar nombre de salida a minúsculas")
flag.BoolVar(&cfg.Uppercase, "uppercase", false, "Normalizar nombre de salida a mayúsculas")
8 months ago
configurarAyuda()
flag.Parse()
return cfg
}
func validateConfig(cfg *Config) error {
if cfg.InputDir == "" || cfg.OutputDir == "" {
flag.Usage()
7 months ago
return fmt.Errorf("faltan parámetros obligatorios: -dir y -out son requeridos")
8 months ago
}
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")
}
7 months ago
if cfg.Format != "" &&
cfg.Format != "jpg" && cfg.Format != "jpeg" &&
cfg.Format != "png" &&
cfg.Format != "tif" && cfg.Format != "tiff" {
return fmt.Errorf("formato de salida no soportado: %s (opciones: jpg, png, tif)", cfg.Format)
8 months ago
}
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)
}
7 months ago
if absInput == absOutput && cfg.Prefix == "" && cfg.Suffix == "" {
return fmt.Errorf(
"el directorio de entrada y salida no pueden ser el mismo\n" +
" consejo: usa -prefijo o -sufijo para procesar en el mismo directorio sin sobreescribir los originales")
8 months ago
}
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
}
7 months ago
// Validar pares de sustitución
for _, raw := range []string{cfg.Replace, cfg.ReplaceI, cfg.ReplaceP} {
if raw == "" {
continue
}
for _, pair := range strings.Split(raw, ",") {
parts := strings.SplitN(pair, "/", 2)
if len(parts) != 2 || parts[0] == "" {
return fmt.Errorf("par de sustitución inválido %q — formato esperado: original/destino", pair)
}
}
}
// Validar glob de filtro
if cfg.FileFilter != "" {
if _, err := filepath.Match(cfg.FileFilter, ""); err != nil {
return fmt.Errorf("patrón -filter inválido %q: %v", cfg.FileFilter, err)
}
}
if cfg.Lowercase && cfg.Uppercase {
return fmt.Errorf("-lowercase y -uppercase son mutuamente excluyentes")
}
8 months ago
return nil
}
7 months ago
// ─────────────────────────────────────────────────────────────────────────────
// Recolección de ficheros
// ─────────────────────────────────────────────────────────────────────────────
func collectImageFiles(root string, glob string) (*ImageSummary, error) {
8 months ago
validExts := map[string]bool{
".jpg": true,
".jpeg": true,
".png": true,
".webp": true,
7 months ago
".tif": true,
".tiff": true,
".bmp": true,
}
readOnlyExts := map[string]bool{
".webp": true,
".bmp": true,
8 months ago
}
7 months ago
summary := &ImageSummary{
ByExt: make(map[string]int),
}
seen := map[string]bool{}
8 months ago
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))
7 months ago
if !validExts[ext] {
return nil
}
// Aplicar glob sobre el nombre del fichero (sin ruta)
if glob != "" {
matched, merr := filepath.Match(glob, filepath.Base(path))
if merr != nil || !matched {
return nil
}
}
summary.Files = append(summary.Files, path)
summary.ByExt[ext]++
if readOnlyExts[ext] && !seen[ext] {
summary.ReadOnlyExts = append(summary.ReadOnlyExts, ext)
seen[ext] = true
8 months ago
}
return nil
})
7 months ago
return summary, err
}
// ─────────────────────────────────────────────────────────────────────────────
// Ruta de salida
// ─────────────────────────────────────────────────────────────────────────────
func buildOutputPath(inputPath string, cfg *Config) string {
relPath, err := filepath.Rel(cfg.InputDir, inputPath)
if err != nil {
relPath = filepath.Base(inputPath)
}
base := filepath.Base(relPath)
dir := filepath.Dir(relPath)
ext := filepath.Ext(base)
stem := base[:len(base)-len(ext)]
// Formato de salida
outExt := ext
if cfg.Format != "" {
outExt = "." + cfg.Format
switch cfg.Format {
case "jpeg":
outExt = ".jpg"
case "tiff":
outExt = ".tif"
}
}
stem = applyStemReplacements(stem, cfg)
// Normalización de case — se aplica sobre el stem ya transformado,
// pero nunca sobre la extensión (evita .JPG o .TIF en sistemas sensibles)
switch {
case cfg.Lowercase:
stem = strings.ToLower(stem)
case cfg.Uppercase:
stem = strings.ToUpper(stem)
}
newName := cfg.Prefix + stem + cfg.Suffix + outExt
if cfg.KeepStructure {
return filepath.Join(cfg.OutputDir, dir, newName)
}
return filepath.Join(cfg.OutputDir, newName)
8 months ago
}
7 months ago
// ─────────────────────────────────────────────────────────────────────────────
// Orquestación de workers
// ─────────────────────────────────────────────────────────────────────────────
8 months ago
func processImages(files []string, cfg *Config) {
var (
wg sync.WaitGroup
7 months ago
mu sync.Mutex
8 months ago
processed atomic.Int32
7 months ago
errCount atomic.Int32
totalIn atomic.Int64
totalOut atomic.Int64
8 months ago
total = int32(len(files))
7 months ago
start = time.Now()
8 months ago
)
jobs := make(chan string, len(files))
for i := 0; i < cfg.Workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for inputPath := range jobs {
result := processImage(inputPath, cfg)
current := processed.Add(1)
7 months ago
mu.Lock()
8 months ago
if result.Success {
7 months ago
totalIn.Add(result.InputSize)
totalOut.Add(result.OutputSize)
8 months ago
if cfg.Verbose {
7 months ago
fmt.Printf(" [%d/%d] ✓ %-40s → %-40s (%s)\n",
current, total,
truncate(result.Filename, 40),
truncate(filepath.Base(result.OutputPath), 40),
result.Duration.Round(time.Millisecond))
} else {
fmt.Printf(" [%d/%d] ✓ %s\n", current, total, result.Filename)
8 months ago
}
} else {
7 months ago
errCount.Add(1)
fmt.Printf(" [%d/%d] ✗ %s: %v\n", current, total, result.Filename, result.Error)
8 months ago
}
7 months ago
mu.Unlock()
8 months ago
}
}()
}
7 months ago
for _, f := range files {
jobs <- f
8 months ago
}
close(jobs)
wg.Wait()
7 months ago
elapsed := time.Since(start).Round(time.Millisecond)
ok := total - errCount.Load()
inMB := float64(totalIn.Load()) / 1024 / 1024
outMB := float64(totalOut.Load()) / 1024 / 1024
var saving string
if totalIn.Load() > 0 {
pct := 100.0 - (float64(totalOut.Load())/float64(totalIn.Load()))*100.0
saving = fmt.Sprintf("%.1f%%", pct)
} else {
saving = "n/a"
}
fmt.Printf("\n")
fmt.Printf(" ══════════════════════════════════════════════════════\n")
fmt.Printf(" Procesamiento completado en %s\n\n", elapsed)
fmt.Printf(" Imágenes %d total / %d exitosas / %d errores\n",
total, ok, errCount.Load())
fmt.Printf(" Tamaño %.2f MB entrada → %.2f MB salida (%s reducción)\n",
inMB, outMB, saving)
fmt.Printf(" ══════════════════════════════════════════════════════\n\n")
8 months ago
}
7 months ago
// ─────────────────────────────────────────────────────────────────────────────
// Procesado individual
// ─────────────────────────────────────────────────────────────────────────────
8 months ago
func processImage(inputPath string, cfg *Config) ProcessResult {
7 months ago
start := time.Now()
8 months ago
result := ProcessResult{
Filename: filepath.Base(inputPath),
}
7 months ago
// Tamaño de entrada
if fi, err := os.Stat(inputPath); err == nil {
result.InputSize = fi.Size()
8 months ago
}
7 months ago
outputPath := buildOutputPath(inputPath, cfg)
result.OutputPath = outputPath
8 months ago
7 months ago
// Crear directorios intermedios
8 months ago
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()
7 months ago
img, srcFormat, err := image.Decode(f)
8 months ago
if err != nil {
result.Error = fmt.Errorf("error decodificando imagen: %v", err)
return result
}
if cfg.Verbose {
bounds := img.Bounds()
7 months ago
log.Printf(" %s: formato=%s tamaño=%dx%d",
result.Filename, srcFormat, bounds.Dx(), bounds.Dy())
8 months ago
}
// Redimensionar
7 months ago
resized := imaging.Resize(img, cfg.Width, cfg.Height, getResampleFilter(cfg.Filter))
8 months ago
// 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
}
}()
outputFormat := cfg.Format
if outputFormat == "" {
7 months ago
ext := strings.ToLower(filepath.Ext(outputPath))
outputFormat = strings.TrimPrefix(ext, ".")
8 months ago
}
switch outputFormat {
case "jpg", "jpeg":
err = jpeg.Encode(out, resized, &jpeg.Options{Quality: cfg.Quality})
case "png":
err = png.Encode(out, resized)
7 months ago
case "tif", "tiff":
err = tiff.Encode(out, resized, &tiff.Options{Compression: tiff.Deflate, Predictor: true})
case "webp", "bmp":
err = fmt.Errorf("formato %s sólo soportado en lectura — usa -formato jpg, png o tif", outputFormat)
8 months ago
default:
7 months ago
err = fmt.Errorf("formato de salida desconocido: %s", outputFormat)
8 months ago
}
if err != nil {
result.Error = fmt.Errorf("error guardando imagen: %v", err)
return result
}
7 months ago
// Tamaño de salida
if fi, err := os.Stat(outputPath); err == nil {
result.OutputSize = fi.Size()
}
8 months ago
result.Success = true
7 months ago
result.Duration = time.Since(start)
8 months ago
return result
}
7 months ago
// ─────────────────────────────────────────────────────────────────────────────
// Utilidades
// ─────────────────────────────────────────────────────────────────────────────
8 months ago
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
}
}
7 months ago
func truncate(s string, max int) string {
if len(s) <= max {
return s
}
return "…" + s[len(s)-(max-1):]
}
// ─────────────────────────────────────────────────────────────────────────────
// Sustitución de tokens en nombres de fichero
// ─────────────────────────────────────────────────────────────────────────────
// applyStemReplacements aplica los tres modos de sustitución al stem del fichero.
// El orden de aplicación es: -replace → -replacei → -replacep.
func applyStemReplacements(stem string, cfg *Config) string {
if cfg.Replace != "" {
for _, p := range parsePairs(cfg.Replace) {
stem = strings.ReplaceAll(stem, p.From, p.To)
}
}
if cfg.ReplaceI != "" {
for _, p := range parsePairs(cfg.ReplaceI) {
stem = replaceInsensitive(stem, p.From, p.To, false)
}
}
if cfg.ReplaceP != "" {
for _, p := range parsePairs(cfg.ReplaceP) {
stem = replaceInsensitive(stem, p.From, p.To, true)
}
}
return stem
}
// parsePairs descompone "orig1/dest1,orig2/dest2" en una slice de ReplacePair.
func parsePairs(raw string) []ReplacePair {
var pairs []ReplacePair
for _, item := range strings.Split(raw, ",") {
parts := strings.SplitN(strings.TrimSpace(item), "/", 2)
if len(parts) == 2 && parts[0] != "" {
pairs = append(pairs, ReplacePair{From: parts[0], To: parts[1]})
}
}
return pairs
}
// replaceInsensitive sustituye todas las ocurrencias de from en s de forma
// case-insensitive. Si preserveCase es true, la sustitución adapta las
// mayúsculas del token de destino para que reflejen las del token encontrado.
func replaceInsensitive(s, from, to string, preserveCase bool) string {
if from == "" {
return s
}
re := regexp.MustCompile(`(?i)` + regexp.QuoteMeta(from))
return re.ReplaceAllStringFunc(s, func(match string) string {
if !preserveCase {
return to
}
return matchCase(match, to)
})
}
// matchCase aplica el patrón de mayúsculas de src a dst.
//
// "imagen" → "picture" (todo minúsculas → todo minúsculas)
// "Imagen" → "Picture" (primera mayúscula → primera mayúscula)
// "IMAGEN" → "PICTURE" (todo mayúsculas → todo mayúsculas)
// "iMaGeN" → "picture" (mixto → minúsculas, caso seguro)
func matchCase(src, dst string) string {
srcRunes := []rune(src)
dstRunes := []rune(dst)
if len(srcRunes) == 0 || len(dstRunes) == 0 {
return dst
}
allUpper := true
allLower := true
for _, r := range srcRunes {
if unicode.IsLetter(r) {
if !unicode.IsUpper(r) {
allUpper = false
}
if !unicode.IsLower(r) {
allLower = false
}
}
}
switch {
case allUpper:
return strings.ToUpper(dst)
case allLower:
return strings.ToLower(dst)
case unicode.IsUpper(srcRunes[0]):
// Primera letra mayúscula, resto minúsculas
out := make([]rune, len(dstRunes))
for i, r := range dstRunes {
if i == 0 {
out[i] = unicode.ToUpper(r)
} else {
out[i] = unicode.ToLower(r)
}
}
return string(out)
default:
return strings.ToLower(dst)
}
}
8 months ago
func configurarAyuda() {
flag.Usage = func() {
7 months ago
fmt.Printf(`
██╗███╗ ███╗ █████╗ ██████╗ ██╗███╗ ██╗ █████╗ ████████╗ ██████╗ ██████╗
██║████╗ ████║██╔══██╗██╔════╝ ██║████╗ ██║██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗
██║██╔████╔██║███████║██║ ███╗██║██╔██╗ ██║███████║ ██║ ██║ ██║██████╔╝
██║██║╚██╔╝██║██╔══██║██║ ██║██║██║╚██╗██║██╔══██║ ██║ ██║ ██║██╔══██╗
██║██║ ╚═╝ ██║██║ ██║╚██████╔╝██║██║ ╚████║██║ ██║ ██║ ╚██████╔╝██║ ██║
╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
IMAGINATOR VER 2.4.0 Juan V. Navarro Leal
──────────────────────────────────────────────────────────────────────────────
ActiveThing activething.com/Imaginator ©2026 ActiveThing
──────────────────────────────────────────────────────────────────────────────
USO
imaginator -dir <origen> -out <destino> { -width N | -height N } [opciones]
PARÁMETROS OBLIGATORIOS
-dir string Directorio de origen con las imágenes
-out string Directorio de destino para imágenes procesadas
Al menos uno de los siguientes:
-width int Ancho objetivo (mantiene proporción si -height no se especifica)
-height int Alto objetivo (mantiene proporción si -width no se especifica)
FORMATOS SOPORTADOS
Lectura jpg · jpeg · png · tif · tiff · bmp · webp
Escritura jpg · png · tif
(bmp y webp son sólo lectura — usa -formato jpg, png o tif para convertir)
OPCIONES DE IMAGEN
-calidad int Calidad JPEG 1–100 [85]
-formato string Formato de salida: jpg, png, tif [mantener original]
-filtro string Filtro: lanczos · cubic · linear · box [lanczos]
OPCIONES DE NOMBRADO
-prefijo string Prefijo para el nombre de salida [""]
-sufijo string Sufijo para el nombre de salida [""]
-lowercase Normalizar nombre de salida a minúsculas [false]
-uppercase Normalizar nombre de salida a mayúsculas [false]
-replace string Sustituir token en nombre: orig/dest[,orig2/dest2]
case sensitive: imagen→picture, Imagen→Imagen
-replacei string Igual que -replace pero case insensitive
imagen→picture, Imagen→picture, IMAGEN→picture
-replacep string Igual que -replacei pero preserva capitalización
imagen→picture, Imagen→Picture, IMAGEN→PICTURE
OPCIONES DE FILTRADO
-filter string Glob sobre el nombre del fichero a procesar [""]
ej: "foto_*.jpg" "DSC_????.png" "*.webp"
OPCIONES DE EJECUCIÓN
-workers int Workers concurrentes [4]
-estructura Mantener estructura de subdirectorios [false]
-dry-run Mostrar qué se procesaría sin ejecutar [false]
-verbose Información detallada por imagen [false]
EJEMPLOS
# Redimensionar a 800 px de ancho, mantener proporción
imaginator -dir ./fotos -out ./thumbs -width 800
# Convertir todo a JPG 1920×1080 con calidad 90
imaginator -dir ./raw -out ./web -width 1920 -height 1080 -formato jpg -calidad 90
# 8 workers, preservar árbol de carpetas, calidad 85
imaginator -dir ./raw -out ./web -width 1024 -workers 8 -estructura -calidad 85
# Procesar en el mismo directorio añadiendo sufijo "_800w"
imaginator -dir ./fotos -out ./fotos -width 800 -sufijo _800w
# Procesar sólo ficheros que coincidan con un glob
imaginator -dir ./raw -out ./web -width 1920 -filter "DSC_*.jpg"
# Renombrar tokens en los ficheros de salida (case sensitive)
imaginator -dir ./raw -out ./web -width 800 -replace "imagen/picture,foto/photo"
# Renombrar preservando capitalización
imaginator -dir ./raw -out ./web -width 800 -replacep "imagen/picture"
# imagen_001.jpg → picture_001.jpg
# Imagen_001.jpg → Picture_001.jpg
# IMAGEN_001.jpg → PICTURE_001.jpg
# Normalizar todos los nombres de salida a minúsculas
imaginator -dir ./raw -out ./web -width 800 -lowercase
# Normalizar lote mixto (jpg+png+webp) todo a jpg
imaginator -dir ./raw -out ./web -width 1920 -formato jpg
─────────────────────────────────────────────────────────────────────────────
"No deseo caminar sobre el agua." — Hermann Hesse, Siddhartha
─────────────────────────────────────────────────────────────────────────────
`, version)
8 months ago
}
}

Powered by TurnKey Linux.