2022-02-01 15:34:41 +00:00
package main
import (
"flag"
"fmt"
"image/jpeg"
"io/fs"
"os"
"path/filepath"
"strings"
2022-02-02 16:37:47 +00:00
"github.com/nfnt/resize"
2022-02-01 15:34:41 +00:00
)
type config struct {
Recursive bool
Diff uint
2022-02-01 15:41:50 +00:00
Quality uint
2022-02-01 15:34:41 +00:00
Replace bool
StartingPath string
Quiet bool
2022-02-02 16:11:01 +00:00
AtLeast uint
2022-02-02 16:37:47 +00:00
MaxWidth uint
MaxHeight uint
2022-02-02 20:47:16 +00:00
IgnoreSuffix bool
2022-02-02 16:11:01 +00:00
2022-02-01 15:34:41 +00:00
}
type stats struct {
FilesProcessed uint64
FilesReplaced uint64
StartingBytes uint64
EndingBytes uint64
}
func main ( ) {
var c config
flag . BoolVar ( & c . Recursive , "recursive" , false , "Process folders recursively (default false)." )
flag . UintVar ( & c . Diff , "diff" , 25 , "Percent difference required to replace original image." )
2022-02-01 15:41:50 +00:00
flag . UintVar ( & c . Quality , "quality" , 85 , "JPG compression quality level." )
2022-02-01 15:34:41 +00:00
flag . BoolVar ( & c . Quiet , "quiet" , false , "Less output - don't print per-file detail" )
2022-02-02 16:11:01 +00:00
flag . UintVar ( & c . AtLeast , "atleast" , 0 , "Ignore images that aren't at least this many kilobytes" )
2022-02-02 20:47:16 +00:00
flag . BoolVar ( & c . IgnoreSuffix , "ignoresuffix" , false , "Ignore suffix on filenames and attempt to treat everything as a JPG" )
2022-02-02 16:37:47 +00:00
flag . UintVar ( & c . MaxWidth , "maxwidth" , 0 , "Maximum width, scale images bigger to fit within this width. 0 ignores. Won't replace unless it meets the diff threashold." )
flag . UintVar ( & c . MaxHeight , "maxheight" , 0 , "Maximum height, scale images bigger to fit within this height. 0 ignores. Won't replace until it meets the diff threashold." )
2022-02-01 15:34:41 +00:00
flag . StringVar ( & c . StartingPath , "startingpath" , "." , "Start from this path instead of current working dir" )
flag . BoolVar ( & c . Replace , "replace" , false , "Replace with compressed versions if criteria are met. Otheriwse, just test and report, don't replace any images. (default false)" )
flag . Parse ( )
fail := false
if c . Diff > 99 {
fmt . Println ( "Fatal: diff out of range (-99)" )
fail = true
}
2022-02-01 15:41:50 +00:00
if c . Quality > 99 {
2022-02-01 15:34:41 +00:00
fmt . Println ( "Fatal: compresslevel out of range (1-99)" )
fail = true
}
s , err := os . Stat ( c . StartingPath )
if err != nil || ! s . IsDir ( ) {
fmt . Println ( "Starting path is invalid" )
fail = true
}
if fail {
flag . Usage ( )
return
}
// Generate list of files to examing
var filelist [ ] string
if ! c . Recursive {
d , err := os . ReadDir ( c . StartingPath )
if err != nil {
fmt . Printf ( "Error reading path: %s" , err . Error ( ) )
}
for _ , f := range d {
if ! f . IsDir ( ) {
2022-02-02 20:47:16 +00:00
ext := strings . ToLower ( filepath . Ext ( f . Name ( ) ) )
if c . IgnoreSuffix || ext == ".jpg" || ext == ".jpeg" {
2022-02-01 15:34:41 +00:00
filelist = append ( filelist , f . Name ( ) )
}
}
}
} else {
filepath . WalkDir ( c . StartingPath , func ( path string , d fs . DirEntry , err error ) error {
ext := strings . ToLower ( filepath . Ext ( path ) )
2022-02-02 20:49:49 +00:00
if ! d . IsDir ( ) && ( c . IgnoreSuffix || ext == ".jpg" || ext == ".jpeg" ) {
2022-02-01 15:34:41 +00:00
filelist = append ( filelist , path )
}
return nil
} )
}
var st stats
for _ , f := range filelist {
st = doSingleImage ( f , c , st )
}
fmt . Println ( "\nEnding stats:" )
fmt . Printf ( "Files processed: %d\nFiles replaced: %d\nStarting Size: %s (%d bytes)\nEnding Size: %s (%d bytes)\n" ,
st . FilesProcessed , st . FilesReplaced , SIFormat ( st . StartingBytes ) , st . StartingBytes , SIFormat ( st . EndingBytes ) , st . EndingBytes )
decrease := st . StartingBytes - st . EndingBytes
diff := uint ( ( float64 ( decrease ) / float64 ( st . StartingBytes ) ) * 100 )
fmt . Printf ( "Total savings %s (%d bytes) or %d%%\n" , SIFormat ( decrease ) , decrease , diff )
}
// doSingleImage processes a single file and updateds the run stats as it goes
func doSingleImage ( f string , c config , s stats ) stats {
st , err := os . Stat ( f )
if err != nil {
fmt . Printf ( "Error reading file %s: %s\n" , f , err . Error ( ) )
return s
}
2022-02-02 16:13:58 +00:00
if st . Size ( ) < int64 ( c . AtLeast ) * 1024 {
2022-02-02 16:11:01 +00:00
s . StartingBytes += uint64 ( st . Size ( ) )
if ! c . Quiet {
2022-02-02 16:14:41 +00:00
fmt . Printf ( "Ignoring %s as %d bytes are below threshold\n" , f , st . Size ( ) )
2022-02-02 16:11:01 +00:00
}
return s
}
2022-02-01 15:34:41 +00:00
in , err := os . Open ( f )
if err != nil {
fmt . Printf ( "Error opening file %s: %s\n" , f , err . Error ( ) )
}
defer in . Close ( )
orig , err := jpeg . Decode ( in )
if err != nil {
fmt . Printf ( "Couldn't decode source image %s: %s\n" , f , err . Error ( ) )
return s
}
in . Close ( )
2022-02-02 16:37:47 +00:00
// Check bounds and resize if necessary
ores := orig . Bounds ( )
if ( c . MaxWidth != 0 && ores . Dx ( ) > int ( c . MaxWidth ) ) || ( c . MaxHeight != 0 && ores . Dy ( ) > int ( c . MaxHeight ) ) {
orig = resize . Thumbnail ( c . MaxWidth , c . MaxHeight , orig , resize . Lanczos3 )
fmt . Printf ( "Resized from %dx%d - " , ores . Dx ( ) , ores . Dy ( ) )
}
2022-02-01 15:34:41 +00:00
out , err := os . OpenFile ( "imageoptimizer.tmp" , os . O_WRONLY | os . O_CREATE | os . O_TRUNC , st . Mode ( ) )
if err != nil {
fmt . Printf ( "Couldn't open out temprary file: %s\n" , err . Error ( ) )
return s
}
2022-02-01 15:41:50 +00:00
err = jpeg . Encode ( out , orig , & jpeg . Options { Quality : int ( c . Quality ) } )
2022-02-01 15:34:41 +00:00
if err != nil {
fmt . Printf ( "Error re-encoding image %s: %s\n" , f , err . Error ( ) )
return s
}
out . Close ( )
stt , err := os . Stat ( "imageoptimizer.tmp" )
if err != nil {
fmt . Printf ( "Error reading re-compressed temp file: %s\n" , err . Error ( ) )
return s
}
s . StartingBytes += uint64 ( st . Size ( ) )
decrease := st . Size ( ) - stt . Size ( )
diff := ( ( float64 ( decrease ) / float64 ( st . Size ( ) ) ) * 100 )
2022-02-02 16:05:46 +00:00
if diff > float64 ( c . Diff ) {
2022-02-01 15:34:41 +00:00
// Would replace
if c . Replace {
err = os . Rename ( f , "imageoptimizer.replacetmp" )
if err != nil {
fmt . Printf ( "Fatal error moving original file %s: %s\n" , f , err . Error ( ) )
os . Exit ( 1 )
}
err = os . Rename ( "imageoptimizer.tmp" , f )
if err != nil {
fmt . Printf ( "Fatal error replacing original file %s: %s\nOriginal file has been renamed to imageoptimizer.replacetmp" , f , err . Error ( ) )
os . Exit ( 1 )
}
os . Remove ( "imageoptimizer.replacetmp" )
}
s . EndingBytes += uint64 ( stt . Size ( ) )
s . FilesReplaced ++
if ( ! c . Quiet ) {
fmt . Printf ( "Replace %s: From %s (%d bytes) to %s (%d bytes) (%.0f%%)\n" ,
f , SIFormat ( uint64 ( st . Size ( ) ) ) , st . Size ( ) , SIFormat ( uint64 ( stt . Size ( ) ) ) , stt . Size ( ) , diff )
}
} else {
if ( ! c . Quiet ) {
fmt . Printf ( "Not Replacing %s: From %s (%d bytes) to %s (%d bytes) (%.0f%%)\n" ,
f , SIFormat ( uint64 ( st . Size ( ) ) ) , st . Size ( ) , SIFormat ( uint64 ( stt . Size ( ) ) ) , stt . Size ( ) , diff )
}
s . EndingBytes += uint64 ( st . Size ( ) )
2022-02-10 15:58:55 +00:00
os . Remove ( "imageoptimizer.tmp" )
2022-02-01 15:34:41 +00:00
}
s . FilesProcessed ++
return s
}
// SIFormat prints sizes in a nice human readable way
func SIFormat ( num_in uint64 ) string {
suffix := "B" //just assume bytes
num := float64 ( num_in )
units := [ ] string { "" , "K" , "M" , "G" , "T" , "P" , "E" , "Z" }
for _ , unit := range units {
if num < 1000.0 {
return fmt . Sprintf ( "%3.1f%s%s" , num , unit , suffix )
}
num = ( num / 1000 )
}
return fmt . Sprintf ( "%.1f%s%s" , num , "Yi" , suffix )
}