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.

273 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
——————————————————————————————————————————————————————————————————————————
Proyecto Imaginator
—————————————————————————————————————————————————————————————————————————
Fichero cmd/cli/main.go
Package main (CLI)
Autor Juan V. Navarro juanvnl@activething.com
Creado 26/01/2026
Mejorado 20/03/2026
—————————————————————————————————————————————————————————————————————————
This software is licensed under the Elastic License v2.0.
https://activething.com/ATGO/licenses
Web : activething.com/Imaginator
Correo : dev@activething.com
"I do not wish to walk on water." — Hermann Hesse, Siddhartha
Copyright (c) 2026 Active Thing
—————————————————————————————————————————————————————————————————————————
*/
package main
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
"imaginator/core"
)
func main() {
cfg := parseFlags()
if err := core.Validate(cfg); err != nil {
log.Fatal(err)
}
if err := os.MkdirAll(cfg.OutputDir, 0755); err != nil {
log.Fatalf("error creating output directory: %v", err)
}
summary, err := core.CollectImageFiles(cfg.InputDir, cfg.FileFilter)
if err != nil {
log.Fatalf("error reading source directory: %v", err)
}
if len(summary.Files) == 0 {
fmt.Println(" No images found to process.")
return
}
// Type breakdown
fmt.Printf(" Found %d images:\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 += " ⚠ read-only"
}
fmt.Println(line)
}
}
if len(summary.ReadOnlyExts) > 0 && cfg.Format == "" {
joined := strings.Join(summary.ReadOnlyExts, ", ")
fmt.Println()
fmt.Printf(" WARNING: batch contains read-only formats (%s).\n", joined)
fmt.Println(" Output files in that format may be corrupted.")
fmt.Println(" Add -format jpg or -format png to convert them.")
}
fmt.Println()
if cfg.DryRun {
fmt.Println(" [dry-run] Output paths:")
fmt.Println()
for _, f := range summary.Files {
out := core.BuildOutputPath(f, cfg)
fmt.Printf(" %s → %s\n", f, out)
}
fmt.Println()
return
}
fmt.Printf(" Processing with %d workers...\n\n", cfg.Workers)
var (
ok int32
errCount int32
totalIn int64
totalOut int64
start = time.Now()
)
total := int32(len(summary.Files))
for p := range core.Run(summary, cfg) {
r := p.Result
if r.Success {
ok++
totalIn += r.InputSize
totalOut += r.OutputSize
if cfg.Verbose {
fmt.Printf(" [%d/%d] ✓ %-40s → %-40s (%s)\n",
p.Current, p.Total,
truncate(r.Filename, 40),
truncate(filepath.Base(r.OutputPath), 40),
r.Duration.Round(time.Millisecond))
} else {
fmt.Printf(" [%d/%d] ✓ %s\n", p.Current, p.Total, r.Filename)
}
} else {
errCount++
fmt.Printf(" [%d/%d] ✗ %s: %v\n", p.Current, p.Total, r.Filename, r.Error)
}
}
elapsed := time.Since(start).Round(time.Millisecond)
inMB := float64(totalIn) / 1024 / 1024
outMB := float64(totalOut) / 1024 / 1024
var saving string
if totalIn > 0 {
pct := 100.0 - (float64(totalOut)/float64(totalIn))*100.0
saving = fmt.Sprintf("%.1f%%", pct)
} else {
saving = "n/a"
}
fmt.Printf("\n")
fmt.Printf(" ══════════════════════════════════════════════════════\n")
fmt.Printf(" Done in %s\n\n", elapsed)
fmt.Printf(" Images %d total / %d ok / %d errors\n", total, ok, errCount)
fmt.Printf(" Size %.2f MB in → %.2f MB out (%s saved)\n", inMB, outMB, saving)
fmt.Printf(" ══════════════════════════════════════════════════════\n\n")
}
func parseFlags() *core.Config {
cfg := core.DefaultConfig()
flag.StringVar(&cfg.InputDir, "dir", "", "Source directory")
flag.StringVar(&cfg.OutputDir, "out", "", "Output directory")
flag.IntVar(&cfg.Width, "width", 0, "Target width")
flag.IntVar(&cfg.Height, "height", 0, "Target height")
flag.IntVar(&cfg.Quality, "quality", cfg.Quality, "JPEG quality (1-100)")
flag.StringVar(&cfg.Format, "format", "", "Output format: jpg, png, tif")
flag.StringVar(&cfg.Resample, "resample", cfg.Resample, "Resampling filter: lanczos, cubic, linear, box")
flag.IntVar(&cfg.Workers, "workers", cfg.Workers, "Concurrent workers")
flag.BoolVar(&cfg.Verbose, "verbose", false, "Verbose output")
flag.BoolVar(&cfg.KeepStructure, "keepstructure", false, "Preserve subdirectory structure")
flag.StringVar(&cfg.Prefix, "prefix", "", "Prefix for output filename")
flag.StringVar(&cfg.Suffix, "suffix", "", "Suffix for output filename (before extension)")
flag.BoolVar(&cfg.DryRun, "dry-run", false, "Show what would be processed without executing")
flag.StringVar(&cfg.Replace, "replace", "", "Replace tokens in filename: old/new[,...] (case sensitive)")
flag.StringVar(&cfg.ReplaceI, "replacei", "", "Replace tokens in filename: old/new[,...] (case insensitive)")
flag.StringVar(&cfg.ReplaceP, "replacep", "", "Replace tokens in filename: old/new[,...] (insensitive+preserve case)")
flag.StringVar(&cfg.FileFilter, "filter", "", "Glob pattern to select input files")
flag.BoolVar(&cfg.Lowercase, "lowercase", false, "Normalize output filename to lowercase")
flag.BoolVar(&cfg.Uppercase, "uppercase", false, "Normalize output filename to uppercase")
flag.StringVar(&cfg.B64Ext, "b64", "", "Encode output as base64 (optional value = extension, default: b64)")
flag.StringVar(&cfg.TSFile, "ts", "", "Write TypeScript file with all images as base64 data URIs")
flag.StringVar(&cfg.NoSpaces, "nospaces", "", "Replace spaces in filename (optional value = replacement char, default: _)")
setupHelp()
flag.Parse()
flag.Visit(func(f *flag.Flag) {
if f.Name == "b64" {
cfg.B64Enabled = true
if cfg.B64Ext == "" {
cfg.B64Ext = "b64"
}
}
if f.Name == "nospaces" {
cfg.NoSpacesOn = true
if cfg.NoSpaces == "" {
cfg.NoSpaces = "_"
}
}
})
return cfg
}
func truncate(s string, max int) string {
if len(s) <= max {
return s
}
return "…" + s[len(s)-(max-1):]
}
func setupHelp() {
flag.Usage = func() {
fmt.Printf(`
██╗███╗ ███╗ █████╗ ██████╗ ██╗███╗ ██╗ █████╗ ████████╗ ██████╗ ██████╗
██║████╗ ████║██╔══██╗██╔════╝ ██║████╗ ██║██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗
██║██╔████╔██║███████║██║ ███╗██║██╔██╗ ██║███████║ ██║ ██║ ██║██████╔╝
██║██║╚██╔╝██║██╔══██║██║ ██║██║██║╚██╗██║██╔══██║ ██║ ██║ ██║██╔══██╗
██║██║ ╚═╝ ██║██║ ██║╚██████╔╝██║██║ ╚████║██║ ██║ ██║ ╚██████╔╝██║ ██║
╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
─────────────────────────────────────────────────────────────────────────────
IMAGINATOR VER %s
ActiveThing activething.com/Imaginator ©2026 ActiveThing
─────────────────────────────────────────────────────────────────────────────
USAGE
imaginator -dir <source> -out <dest> { -width N | -height N } [options]
REQUIRED
-dir string Source directory
-out string Output directory
At least one of:
-width int Target width (keeps aspect ratio if -height is omitted)
-height int Target height (keeps aspect ratio if -width is omitted)
SUPPORTED FORMATS
Read jpg · jpeg · png · tif · tiff · bmp · webp
Write jpg · png · tif
(bmp and webp are read-only — use -format jpg, png or tif to convert)
IMAGE OPTIONS
-quality int JPEG quality 1–100 [85]
-format string Output format: jpg, png, tif [keep original]
-resample string Resampling filter: lanczos · cubic · linear · box [lanczos]
-b64 string Encode output as base64; optional value = file extension [b64]
-ts string Write TypeScript file with all images as base64 data URIs
(-b64 and -ts are mutually exclusive)
NAMING OPTIONS
-prefix string Prefix for output filename [""]
-suffix string Suffix for output filename (before extension) [""]
-lowercase Normalize output filename to lowercase [false]
-uppercase Normalize output filename to uppercase [false]
-nospaces string Replace spaces; optional value = replacement char [_]
-replace string Replace token: old/new[,old2/new2] (case sensitive)
-replacei string Same as -replace but case insensitive
-replacep string Same as -replacei but preserves capitalization
FILTER OPTIONS
-filter string Glob pattern on filename [""]
EXECUTION OPTIONS
-workers int Concurrent workers [4]
-keepstructure Preserve subdirectory structure [false]
-dry-run Show what would be processed without running [false]
-verbose Verbose output per image [false]
EXAMPLES
imaginator -dir ./photos -out ./thumbs -width 800
imaginator -dir ./raw -out ./web -width 1920 -height 1080 -format jpg -quality 90
imaginator -dir ./icons -out ./dist -width 64 -format png -ts src/assets/icons.ts
imaginator -dir ./raw -out ./web -width 800 -nospaces -lowercase
─────────────────────────────────────────────────────────────────────────────
"I do not wish to walk on water." — Hermann Hesse, Siddhartha
─────────────────────────────────────────────────────────────────────────────
`, core.Version)
}
}

Powered by TurnKey Linux.