// Package main is the main application package main import ( "fmt" "os" "os/exec" "regexp" "runtime" "strconv" "sync" "github.com/axllent/ghru/v2" "github.com/spf13/pflag" ) var ( quality int maxWidth int maxHeight int outputDir string preserveModTimes bool jpegoptim string jpegtran string optipng string pngquant string gifsicle string copyExif bool threads = 1 version = "dev" // ghruConf is the configuration for the ghru package ghruConf = ghru.Config{ Repo: "axllent/goptimize", ArchiveName: "goptimize-{{.OS}}-{{.Arch}}", BinaryName: "goptimize", CurrentVersion: version, } ) func main() { // set up new flag instance flag := pflag.NewFlagSet(os.Args[0], pflag.ExitOnError) // set the default help flag.Usage = func() { fmt.Println("Goptimize - downscales and optimizes images") fmt.Printf("\nUsage: %s [options] \n", os.Args[0]) fmt.Println("\nOptions:") flag.SortFlags = false flag.PrintDefaults() fmt.Println("\nExamples:") fmt.Printf(" %s image.png\n", os.Args[0]) fmt.Printf(" %s -m 800x800 *.jpg\n", os.Args[0]) fmt.Printf(" %s -o out/ -q 90 -m 1600x1600 *.jpg\n", os.Args[0]) fmt.Println("\nDetected optimizers:") if err := displayDetectedOptimizer("jpegtran ", jpegtran); err != nil { _ = displayDetectedOptimizer("jpegoptim", jpegoptim) } _ = displayDetectedOptimizer("optipng ", optipng) _ = displayDetectedOptimizer("pngquant ", pngquant) _ = displayDetectedOptimizer("gifsicle ", gifsicle) } var maxSizes string var multiThreaded, update, showVersion, showHelp bool flag.IntVarP(&quality, "quality", "q", 75, "quality, JPEG only") flag.StringVarP(&maxSizes, "max", "m", "", "downscale to a maximum width & height in pixels (x)") flag.StringVarP(&outputDir, "out", "o", "", "output directory (default overwrites original)") flag.BoolVarP(&preserveModTimes, "preserve", "p", true, "preserve file modification times") flag.BoolVarP(©Exif, "exif", "e", false, "copy exif data") flag.BoolVarP(&update, "update", "u", false, "update to latest release") flag.BoolVarP(&multiThreaded, "threaded", "t", false, "run multi-threaded (use all CPU cores)") flag.BoolVarP(&showVersion, "version", "v", false, "show version number") flag.BoolVarP(&showHelp, "help", "h", false, "show help") // third-party optimizers flag.StringVar(&jpegtran, "jpegtran", "jpegtran", "jpegtran binary") flag.StringVar(&jpegoptim, "jpegoptim", "jpegoptim", "jpegoptim binary") flag.StringVar(&gifsicle, "gifsicle", "gifsicle", "gifsicle binary") flag.StringVar(&pngquant, "pngquant", "pngquant", "pngquant binary") flag.StringVar(&optipng, "optipng", "optipng", "optipng binary") flag.SortFlags = false // parse args excluding os.Args[0] if err := flag.Parse(os.Args[1:]); err != nil { fmt.Printf("Error parsing flags: %s\n", err.Error()) } // detect optimizer paths gifsicle, _ = exec.LookPath(gifsicle) jpegoptim, _ = exec.LookPath(jpegoptim) jpegtran, _ = exec.LookPath(jpegtran) optipng, _ = exec.LookPath(optipng) pngquant, _ = exec.LookPath(pngquant) if showHelp { flag.Usage() os.Exit(1) } if showVersion { fmt.Printf("Version: %s\n", version) release, err := ghruConf.Latest() if err != nil { fmt.Printf("Error getting latest release: %s\n", err.Error()) os.Exit(1) } // The latest version is the same version if release.Tag == version { os.Exit(0) } // A newer release is available fmt.Printf( "Update available: %s\nRun `%s -u` to update (requires read/write access to install directory).\n", release.Tag, os.Args[0], ) os.Exit(0) } if update { // Update the app rel, err := ghruConf.SelfUpdate() if err != nil { fmt.Println(err.Error()) os.Exit(1) } fmt.Printf("Updated %s to version %s\n", os.Args[0], rel.Tag) os.Exit(0) } if len(flag.Args()) < 1 { flag.Usage() os.Exit(1) } if maxSizes != "" { // calculate max sizes from arg[0] r := regexp.MustCompile(`^(\d+)(x|X|\*|:)(\d+)$`) matches := r.FindStringSubmatch(maxSizes) if len(matches) != 4 { flag.Usage() os.Exit(1) } maxWidth, _ = strconv.Atoi(matches[1]) maxHeight, _ = strconv.Atoi(matches[3]) } // parse arguments args := flag.Args() if outputDir != "" { // ensure the output directory exists if _, err := os.Stat(outputDir); os.IsNotExist(err) { err := os.MkdirAll(outputDir, os.ModePerm) if err != nil { fmt.Printf("Cannot create output directory: %s\n", outputDir) os.Exit(1) } } } if multiThreaded { threads = runtime.NumCPU() } processChan := make(chan string) wg := &sync.WaitGroup{} // Signal to main goroutine that worker goroutines are working/done working wg.Add(threads) for i := 0; i < threads; i++ { go func() { for nextFile := range processChan { goptimize(nextFile) } // Channel was closed, so we finished this goroutine. wg.Done() // Let main goroutine know we are done. }() } for _, img := range args { processChan <- img } // Close the channel. This tells the worker goroutines that they can be done. close(processChan) // Wait for all worker goroutines to finish processing the IPs wg.Wait() } // displayDetectedOptimizer prints whether the optimizer was found func displayDetectedOptimizer(name, bin string) error { exe, err := exec.LookPath(bin) if err != nil { return err } fmt.Printf(" - %s: %s\n", name, exe) return nil }