package main import ( "bytes" "encoding/base64" "flag" "fmt" "image" "image/jpeg" "image/png" "os" "path/filepath" "strings" "golang.org/x/image/draw" ) type Config struct { dir string width int height int quality int typescript bool tsgroup bool } func main() { cfg := parseFlags() // Verificar que el directorio existe if _, err := os.Stat(cfg.dir); os.IsNotExist(err) { fmt.Printf("Error: El directorio '%s' no existe\n", cfg.dir) os.Exit(1) } // Validar que solo se especifica width o height, no ambos if cfg.width > 0 && cfg.height > 0 { fmt.Println("Error: No puedes especificar -width y -height simultáneamente") fmt.Println("La relación de aspecto se mantiene automáticamente") os.Exit(1) } // Validar que no se usen -ts y -tsgroup juntos if cfg.typescript && cfg.tsgroup { fmt.Println("Error: No puedes usar -ts y -tsgroup simultáneamente") os.Exit(1) } // Leer archivos del directorio entries, err := os.ReadDir(cfg.dir) if err != nil { fmt.Printf("Error al leer el directorio: %v\n", err) os.Exit(1) } // Si es tsgroup, procesar todas las imágenes en un solo archivo if cfg.tsgroup { processTsGroup(entries, cfg) return } // Procesamiento individual processed := 0 for _, entry := range entries { if entry.IsDir() { continue } // Verificar si es una imagen soportada ext := strings.ToLower(filepath.Ext(entry.Name())) if ext != ".jpg" && ext != ".jpeg" && ext != ".png" { continue } inputPath := filepath.Join(cfg.dir, entry.Name()) var outputPath string if cfg.typescript { outputPath = strings.TrimSuffix(inputPath, ext) + ".b64.ts" } else { outputPath = strings.TrimSuffix(inputPath, ext) + ".base64" } fmt.Printf("Procesando: %s\n", entry.Name()) if err := processImage(inputPath, outputPath, cfg); err != nil { fmt.Printf(" Error: %v\n", err) continue } fmt.Printf(" ✓ Generado: %s\n", filepath.Base(outputPath)) processed++ } fmt.Printf("\n%d imágenes procesadas correctamente\n", processed) } func parseFlags() Config { cfg := Config{} flag.StringVar(&cfg.dir, "dir", "", "Directorio con las imágenes (requerido)") flag.IntVar(&cfg.width, "width", 0, "Ancho máximo en píxeles (mantiene proporción)") flag.IntVar(&cfg.height, "height", 0, "Alto máximo en píxeles (mantiene proporción)") flag.IntVar(&cfg.quality, "quality", 80, "Calidad JPEG (1-100)") flag.BoolVar(&cfg.typescript, "ts", false, "Generar archivos TypeScript individuales (.b64.ts)") flag.BoolVar(&cfg.tsgroup, "tsgroup", false, "Generar un único archivo TypeScript con todas las imágenes") flag.Usage = func() { fmt.Fprintf(os.Stderr, "\n╔════════════════════════════════════════════════════════════════════╗\n") fmt.Fprintf(os.Stderr, "║ Procesador de Imágenes a Base64 / TypeScript ║\n") fmt.Fprintf(os.Stderr, "╚════════════════════════════════════════════════════════════════════╝\n\n") fmt.Fprintf(os.Stderr, "DESCRIPCIÓN:\n") fmt.Fprintf(os.Stderr, " Redimensiona imágenes y las convierte a Base64 o archivos TypeScript.\n") fmt.Fprintf(os.Stderr, " Mantiene la relación de aspecto automáticamente.\n\n") fmt.Fprintf(os.Stderr, "USO:\n") fmt.Fprintf(os.Stderr, " %s -dir [opciones]\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, "PARÁMETROS:\n") fmt.Fprintf(os.Stderr, " -dir string\n") fmt.Fprintf(os.Stderr, " Directorio con las imágenes a procesar (REQUERIDO)\n") fmt.Fprintf(os.Stderr, " Formatos soportados: JPG, JPEG, PNG\n\n") fmt.Fprintf(os.Stderr, " -width int\n") fmt.Fprintf(os.Stderr, " Ancho máximo en píxeles (mantiene proporción)\n") fmt.Fprintf(os.Stderr, " No se puede usar junto con -height\n") fmt.Fprintf(os.Stderr, " Por defecto: 800\n\n") fmt.Fprintf(os.Stderr, " -height int\n") fmt.Fprintf(os.Stderr, " Alto máximo en píxeles (mantiene proporción)\n") fmt.Fprintf(os.Stderr, " No se puede usar junto con -width\n\n") fmt.Fprintf(os.Stderr, " -quality int\n") fmt.Fprintf(os.Stderr, " Calidad de compresión JPEG (1-100)\n") fmt.Fprintf(os.Stderr, " Por defecto: 80\n\n") fmt.Fprintf(os.Stderr, " -ts\n") fmt.Fprintf(os.Stderr, " Generar archivos TypeScript individuales (.b64.ts)\n") fmt.Fprintf(os.Stderr, " Formato: export const nombre = \"data:image/jpeg;base64,...\"\n") fmt.Fprintf(os.Stderr, " No se puede usar junto con -tsgroup\n\n") fmt.Fprintf(os.Stderr, " -tsgroup\n") fmt.Fprintf(os.Stderr, " Generar un único archivo TypeScript con todas las imágenes\n") fmt.Fprintf(os.Stderr, " Archivo: nombre-directorio.b64.ts\n") fmt.Fprintf(os.Stderr, " No se puede usar junto con -ts\n\n") fmt.Fprintf(os.Stderr, " -h, -help\n") fmt.Fprintf(os.Stderr, " Muestra esta ayuda\n\n") fmt.Fprintf(os.Stderr, "EJEMPLOS:\n") fmt.Fprintf(os.Stderr, " # Uso básico (ancho 800px por defecto)\n") fmt.Fprintf(os.Stderr, " %s -dir ./imagenes\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, " # Redimensionar a 2480px de ancho para PDF A4 a 300 DPI\n") fmt.Fprintf(os.Stderr, " %s -dir ./imagenes -width 2480\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, " # Redimensionar por altura con calidad máxima\n") fmt.Fprintf(os.Stderr, " %s -dir ./imagenes -height 3508 -quality 95\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, " # Generar archivos TypeScript individuales\n") fmt.Fprintf(os.Stderr, " %s -dir ./imagenes -width 800 -ts\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, " # Generar un único archivo TypeScript con todas las imágenes\n") fmt.Fprintf(os.Stderr, " %s -dir ./imagenes -width 800 -tsgroup\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, " # Dimensiones para visualización web\n") fmt.Fprintf(os.Stderr, " %s -dir ./imagenes -width 1240 -quality 85\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, "DIMENSIONES RECOMENDADAS PARA PDF:\n") fmt.Fprintf(os.Stderr, " ┌─────────────────────┬─────────┬─────────────────┐\n") fmt.Fprintf(os.Stderr, " │ Uso │ Ancho │ DPI │\n") fmt.Fprintf(os.Stderr, " ├─────────────────────┼─────────┼─────────────────┤\n") fmt.Fprintf(os.Stderr, " │ Impresión A4 │ 2480 px │ 300 DPI │\n") fmt.Fprintf(os.Stderr, " │ Pantalla A4 │ 1240 px │ 150 DPI │\n") fmt.Fprintf(os.Stderr, " │ Web/Email │ 800 px │ 96 DPI │\n") fmt.Fprintf(os.Stderr, " └─────────────────────┴─────────┴─────────────────┘\n\n") fmt.Fprintf(os.Stderr, "NOTAS:\n") fmt.Fprintf(os.Stderr, " • La relación de aspecto se mantiene automáticamente\n") fmt.Fprintf(os.Stderr, " • No puedes usar -width y -height simultáneamente\n") fmt.Fprintf(os.Stderr, " • No puedes usar -ts y -tsgroup simultáneamente\n") fmt.Fprintf(os.Stderr, " • Las imágenes más pequeñas que el tamaño especificado no se agrandan\n") fmt.Fprintf(os.Stderr, " • Archivos generados:\n") fmt.Fprintf(os.Stderr, " - Sin flags: nombreoriginal.base64\n") fmt.Fprintf(os.Stderr, " - Con -ts: nombreoriginal.b64.ts (uno por imagen)\n") fmt.Fprintf(os.Stderr, " - Con -tsgroup: nombre-directorio.b64.ts (todas las imágenes)\n\n") } flag.Parse() if cfg.dir == "" { flag.Usage() os.Exit(1) } // Valores por defecto si no se especifica nada if cfg.width == 0 && cfg.height == 0 { cfg.width = 800 } if cfg.quality < 1 || cfg.quality > 100 { fmt.Println("Error: La calidad debe estar entre 1 y 100") os.Exit(1) } return cfg } func processImage(inputPath, outputPath string, cfg Config) error { // Abrir imagen file, err := os.Open(inputPath) if err != nil { return fmt.Errorf("error al abrir archivo: %w", err) } defer file.Close() // Decodificar imagen img, format, err := image.Decode(file) if err != nil { return fmt.Errorf("error al decodificar imagen: %w", err) } // Redimensionar imagen resizedImg := resizeImage(img, cfg.width, cfg.height) // Convertir a bytes var buf bytes.Buffer switch format { case "jpeg", "jpg": err = jpeg.Encode(&buf, resizedImg, &jpeg.Options{Quality: cfg.quality}) case "png": err = png.Encode(&buf, resizedImg) default: err = jpeg.Encode(&buf, resizedImg, &jpeg.Options{Quality: cfg.quality}) } if err != nil { return fmt.Errorf("error al codificar imagen: %w", err) } // Convertir a base64 base64Str := base64.StdEncoding.EncodeToString(buf.Bytes()) // Preparar contenido según el formato var content string if cfg.typescript { // Generar nombre de variable desde el nombre del archivo varName := generateVarName(filepath.Base(inputPath)) content = fmt.Sprintf("export const %s = \"data:image/jpeg;base64,%s\"\n", varName, base64Str) } else { content = base64Str } // Guardar archivo err = os.WriteFile(outputPath, []byte(content), 0644) if err != nil { return fmt.Errorf("error al guardar archivo: %w", err) } return nil } func resizeImage(img image.Image, maxW, maxH int) image.Image { bounds := img.Bounds() width := bounds.Dx() height := bounds.Dy() var newWidth, newHeight int if maxW > 0 && maxH == 0 { // Solo se especificó ancho ratio := float64(height) / float64(width) newWidth = maxW newHeight = int(float64(newWidth) * ratio) } else if maxH > 0 && maxW == 0 { // Solo se especificó alto ratio := float64(width) / float64(height) newHeight = maxH newWidth = int(float64(newHeight) * ratio) } else { // Valores por defecto (no debería llegar aquí por validación) ratio := float64(width) / float64(height) newWidth = maxW newHeight = int(float64(newWidth) / ratio) } // Si la imagen ya es más pequeña, no redimensionar if width <= newWidth && height <= newHeight { return img } // Crear nueva imagen dst := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight)) // Redimensionar con interpolación de alta calidad draw.CatmullRom.Scale(dst, dst.Bounds(), img, bounds, draw.Over, nil) return dst } func generateVarName(filename string) string { // Remover extensión name := strings.TrimSuffix(filename, filepath.Ext(filename)) // Reemplazar caracteres no válidos para variables name = strings.Map(func(r rune) rune { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') { return r } return '_' }, name) // Si empieza con número, añadir prefijo if len(name) > 0 && name[0] >= '0' && name[0] <= '9' { name = "img_" + name } // Si está vacío, usar nombre por defecto if name == "" { name = "image" } return name } func processTsGroup(entries []os.DirEntry, cfg Config) { // Obtener nombre del directorio dirName := filepath.Base(cfg.dir) outputPath := filepath.Join(cfg.dir, dirName+".b64.ts") var tsContent strings.Builder tsContent.WriteString("// Imágenes generadas automáticamente\n\n") processed := 0 for _, entry := range entries { if entry.IsDir() { continue } // Verificar si es una imagen soportada ext := strings.ToLower(filepath.Ext(entry.Name())) if ext != ".jpg" && ext != ".jpeg" && ext != ".png" { continue } inputPath := filepath.Join(cfg.dir, entry.Name()) fmt.Printf("Procesando: %s\n", entry.Name()) // Procesar imagen a base64 base64Str, err := imageToBase64(inputPath, cfg) if err != nil { fmt.Printf(" Error: %v\n", err) continue } // Generar nombre de variable varName := generateVarName(entry.Name()) // Añadir al contenido TypeScript tsContent.WriteString(fmt.Sprintf("export const %s = \"data:image/jpeg;base64,%s\"\n\n", varName, base64Str)) fmt.Printf(" ✓ Añadido: %s\n", varName) processed++ } if processed == 0 { fmt.Println("No se encontraron imágenes para procesar") return } // Guardar archivo err := os.WriteFile(outputPath, []byte(tsContent.String()), 0644) if err != nil { fmt.Printf("Error al guardar archivo: %v\n", err) os.Exit(1) } fmt.Printf("\n✓ Archivo generado: %s\n", filepath.Base(outputPath)) fmt.Printf("%d imágenes procesadas correctamente\n", processed) } func imageToBase64(inputPath string, cfg Config) (string, error) { // Abrir imagen file, err := os.Open(inputPath) if err != nil { return "", fmt.Errorf("error al abrir archivo: %w", err) } defer file.Close() // Decodificar imagen img, format, err := image.Decode(file) if err != nil { return "", fmt.Errorf("error al decodificar imagen: %w", err) } // Redimensionar imagen resizedImg := resizeImage(img, cfg.width, cfg.height) // Convertir a bytes var buf bytes.Buffer switch format { case "jpeg", "jpg": err = jpeg.Encode(&buf, resizedImg, &jpeg.Options{Quality: cfg.quality}) case "png": err = png.Encode(&buf, resizedImg) default: err = jpeg.Encode(&buf, resizedImg, &jpeg.Options{Quality: cfg.quality}) } if err != nil { return "", fmt.Errorf("error al codificar imagen: %w", err) } // Convertir a base64 return base64.StdEncoding.EncodeToString(buf.Bytes()), nil }