From f84a1252dc57f39510110421a4643b5218e56542 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Fri, 2 Aug 2019 23:41:13 +1200 Subject: [PATCH] Overwrite files instead of moving --- goptimize.go | 147 +++++++++++++++++++++++++++++++-------------------- main.go | 22 ++++---- 2 files changed, 100 insertions(+), 69 deletions(-) diff --git a/goptimize.go b/goptimize.go index 4a87988..9648830 100644 --- a/goptimize.go +++ b/goptimize.go @@ -20,23 +20,28 @@ import ( // Goptimize downscales and optimizes an existing image func Goptimize(file string) { info, err := os.Stat(file) + if err != nil { fmt.Printf("%s doesn't exist\n", file) return } + if !info.Mode().IsRegular() { // not a file fmt.Printf("%s is not a file\n", file) return } + // open original, rotate if neccesary src, err := imaging.Open(file, imaging.AutoOrientation(true)) + if err != nil { fmt.Printf("%v (%s)\n", err, file) return } format, err := imaging.FormatFromFilename(file) + if err != nil { fmt.Printf("Cannot detect format: %v\n", err) return @@ -45,6 +50,7 @@ func Goptimize(file string) { outFilename := filepath.Base(file) outDir := filepath.Dir(file) dstFile := filepath.Join(outDir, outFilename) + if outputDir != "" { dstFile = filepath.Join(outputDir, outFilename) } @@ -59,22 +65,24 @@ func Goptimize(file string) { if imgMaxW == 0 || imgMaxW > srcW { imgMaxW = srcW } + imgMaxH := maxHeight if imgMaxH == 0 || imgMaxH > srcH { imgMaxH = srcH } resized := imaging.Fit(src, imgMaxW, imgMaxH, imaging.Lanczos) - dstBounds := resized.Bounds() resultW := dstBounds.Dx() resultH := dstBounds.Dy() tmpFile, err := ioutil.TempFile(os.TempDir(), "Goptimized-") + if err != nil { fmt.Printf("Cannot create temporary file: %v\n", err) return } + defer os.Remove(tmpFile.Name()) if format.String() == "JPEG" { @@ -88,29 +96,30 @@ func Goptimize(file string) { } else if format.String() == "BMP" { err = bmp.Encode(tmpFile, resized) } else { - fmt.Printf("Cannot Goptimize %s files\n", format.String()) + fmt.Printf("Unsupported file type %s\n", file) return } + if err != nil { fmt.Printf("Error saving output file: %v\n", err) return } - // get the tempoary filename before closing + // get the temporary filename before closing tmpFilename := tmpFile.Name() - // close the temp file to release pointers so we can - // modify it with system processes + + // immediately close the temp file to release pointers + // so we can modify it with system processes tmpFile.Close() - // optimize + // Run through optimizers if format.String() == "JPEG" { - // run one or the other, both don't do anything + // run one or the other, running both has no advantage if jpegtran != "" { RunOptimiser(tmpFilename, true, jpegtran, "-optimize", "-outfile") } else if jpegoptim != "" { RunOptimiser(tmpFilename, false, jpegoptim, "-f", "-s", "-o") } - } else if format.String() == "PNG" { if pngquant != "" { RunOptimiser(tmpFilename, true, pngquant, "-f", "--output") @@ -118,13 +127,11 @@ func Goptimize(file string) { if optipng != "" { RunOptimiser(tmpFilename, true, optipng, "-out") } - } else if format.String() == "GIF" { - if gifsicle != "" { - RunOptimiser(tmpFilename, true, gifsicle, "-o") - } + } else if format.String() == "GIF" && gifsicle != "" { + RunOptimiser(tmpFilename, true, gifsicle, "-o") } - // re-open potentiall modified temporary file + // re-open modified temporary file tmpFile, err = os.Open(tmpFilename) if err != nil { fmt.Printf("Error reopening temporary file: %v\n", err) @@ -133,69 +140,101 @@ func Goptimize(file string) { defer tmpFile.Close() - // original file stats + // get th eoriginal file stats srcStat, _ := os.Stat(file) srcSize := srcStat.Size() - // optimized file stats + // get the optimized file stats dstStat, _ := tmpFile.Stat() dstSize := dstStat.Size() - // transfer the original file permissions to the new file - if err = os.Chmod(tmpFile.Name(), srcStat.Mode()); err != nil { - fmt.Printf("Error setting file permissions: %v\n", err) - return - } - - if !skipPreserveModTimes { - // transfer original modification times - mtime := srcStat.ModTime() - atime := mtime // use mtime as we cannot get atime - if err := os.Chtimes(tmpFile.Name(), atime, mtime); err != nil { - fmt.Printf("Error setting file timestamp: %v\n", err) - } - } + // get the original modification time for later + mtime := srcStat.ModTime() + atime := mtime // use mtime as we cannot get atime + // calculate saved percent savedPercent := 100 - math.Round(float64(dstSize)/float64(srcSize)*100) if dstSize < srcSize { - // output is smaller - if err := os.Rename(tmpFile.Name(), dstFile); err != nil { - fmt.Printf("Error renaming file: %v\n", err) + // (over)write the file - not all filesystems support + // cross-filesystem moving so we overwrite the original + out, err := os.Create(dstFile) + if err != nil { + fmt.Printf("Error opening original file: %v\n", err) return } - fmt.Printf("Goptimized %s (%dx%d %s/%s %v%%)\n", dstFile, resultW, resultH, ByteCountSI(dstSize), ByteCountSI(srcSize), savedPercent) - } else { - if outputDir != "" { - // just copy the original - if err := os.Rename(file, dstFile); err != nil { - fmt.Printf("Error renaming file: %v\n", err) - return + + defer out.Close() + + if _, err := io.Copy(out, tmpFile); err != nil { + fmt.Printf("Error ovewriting original file: %v\n", err) + return + } + + if !skipPreserveModTimes { + // transfer original modification times + if err := os.Chtimes(dstFile, atime, mtime); err != nil { + fmt.Printf("Error setting file timestamp: %v\n", err) } } - // we didn't actually any scaling optimizing - fmt.Printf("Goptimized %s (%dx%d %s/%s %v%%)\n", dstFile, srcW, srcH, ByteCountSI(srcSize), ByteCountSI(srcSize), 0) + + fmt.Printf("Goptimized %s (%dx%d %s->%s %v%%)\n", dstFile, resultW, resultH, ByteCountSI(srcSize), ByteCountSI(dstSize), savedPercent) + } else { + // If the output directory is not the same, + // then write a copy of the original file + if outputDir != "" { + out, err := os.Create(dstFile) + if err != nil { + fmt.Printf("Error opening original file: %v\n", err) + return + } + + defer out.Close() + + orig, _ := os.Open(file) + + defer orig.Close() + + if _, err := io.Copy(out, orig); err != nil { + fmt.Printf("Error ovewriting original file: %v\n", err) + return + } + + if !skipPreserveModTimes { + // transfer original modification times + if err := os.Chtimes(dstFile, atime, mtime); err != nil { + fmt.Printf("Error setting file timestamp: %v\n", err) + } + } + + fmt.Printf("Copied %s (%dx%d %s->%s %v%%)\n", dstFile, srcW, srcH, ByteCountSI(srcSize), ByteCountSI(srcSize), 0) + } else { + // we didn't actually change anything + fmt.Printf("Skipped %s (%dx%d %s->%s %v%%)\n", dstFile, srcW, srcH, ByteCountSI(srcSize), ByteCountSI(srcSize), 0) + } } } -// RunOptimiser will run the specified command on a copy of the original file -// and overwrite if the output is smaller than the original -func RunOptimiser(src string, outfile bool, args ...string) { +// RunOptimiser will run the specified command on a copy of the temporary file, +// and overwrite it if the output is smaller than the original +func RunOptimiser(src string, outFileArg bool, args ...string) { // create a new temp file tmpFile, err := ioutil.TempFile(os.TempDir(), "Goptimized-") + if err != nil { fmt.Printf("Cannot create temporary file: %v\n", err) return } + defer os.Remove(tmpFile.Name()) source, err := os.Open(src) - // s, _ := source.Stat() - // log.Printf("%v\n", s.Size()) + if err != nil { fmt.Printf("Cannot open temporary file: %v\n", err) return } + defer source.Close() if _, err := io.Copy(tmpFile, source); err != nil { @@ -205,23 +244,18 @@ func RunOptimiser(src string, outfile bool, args ...string) { // add the filename to the args args = append(args, tmpFile.Name()) - if outfile { - // most commands require a second filename to overwrite the original + if outFileArg { + // most commands require a second filename arg to overwrite the original args = append(args, tmpFile.Name()) } - // fmt.Println(args) - // execute the command cmd := exec.Command(args[0], args[1:]...) - err = cmd.Run() - // out, err := cmd.Output() - if err != nil { - // there was an error + + if err := cmd.Run(); err != nil { fmt.Printf("%s: %v\n", args[0], err) return } - // fmt.Println(string(out)) tmpFilename := tmpFile.Name() @@ -239,9 +273,6 @@ func RunOptimiser(src string, outfile bool, args ...string) { fmt.Printf("Error renaming file: %v\n", err) return } - // fmt.Println(args[0], "=", srcSize, "to", dstSize, "(wrote to", source.Name(), ")") - } else { - // fmt.Println(args[0], "!=", srcSize, "to", dstSize) } } diff --git a/main.go b/main.go index c7abd67..d45c565 100644 --- a/main.go +++ b/main.go @@ -23,17 +23,17 @@ var ( ) func main() { - // modify the default help + // set the default help flag.Usage = func() { - fmt.Println("Re-save & resample images, with optional optimization.") + fmt.Println("Goptimize - Resample optimized images") fmt.Printf("\nUsage: %s [options] \n", os.Args[0]) fmt.Println("\nOptions:") flag.PrintDefaults() fmt.Println("\nExamples:") - fmt.Printf(" %s -s 800x800 *.jpg\n", os.Args[0]) - fmt.Printf(" %s -o out/ -q 90 -s 1600x1600 *.jpg\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("\nOtimizers:") + fmt.Println("\nDetected optimizers:") if err := displayDelectedOptimizer("jpegtran ", jpegtran); err != nil { displayDelectedOptimizer("jpegoptim", jpegoptim) } @@ -44,17 +44,17 @@ func main() { var maxSizes string - flag.IntVar(&quality, "q", 75, "Quality - affects jpeg only") + flag.IntVar(&quality, "q", 75, "Quality - JPEG only") flag.StringVar(&outputDir, "o", "", "Output directory (default overwrites original)") flag.BoolVar(&skipPreserveModTimes, "n", false, "Do not preserve file modification times") flag.StringVar(&maxSizes, "m", "", "Scale down to a maximum width & height. Format must be x.") // third-party optimizers - flag.StringVar(&gifsicle, "gifsicle", "gifsicle", "Alternative gifsicle name") - flag.StringVar(&jpegoptim, "jpegoptim", "jpegoptim", "Alternative jpegoptim name") - flag.StringVar(&jpegtran, "jpegtran", "jpegtran", "Alternative jpegtran name") - flag.StringVar(&optipng, "optipng", "optipng", "Alternative optipng name") - flag.StringVar(&pngquant, "pngquant", "pngquant", "Alternative pngquant name") + flag.StringVar(&gifsicle, "gifsicle", "gifsicle", "gifsicle binary") + flag.StringVar(&jpegoptim, "jpegoptim", "jpegoptim", "jpegoptim binary") + flag.StringVar(&jpegtran, "jpegtran", "jpegtran", "jpegtran binary") + flag.StringVar(&optipng, "optipng", "optipng", "optipng binary") + flag.StringVar(&pngquant, "pngquant", "pngquant", "pngquant binary") // parse flags flag.Parse()