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

7 months ago
/*
——————————————————————————————————————————————————————————————————————————
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.