diff --git a/main.go b/main.go index 5a5dee9..9b6f235 100644 --- a/main.go +++ b/main.go @@ -1,248 +1,301 @@ -package main - -import ( - "bufio" - "flag" - "fmt" - "image/jpeg" - "io/fs" - "os" - "path/filepath" - "runtime" - "strings" - "sync" - "time" - - "github.com/adrium/goheif" - "github.com/mojitaurelie/heic2jpg/exif" -) - -type ( - filer interface { - IsDir() bool - Name() string - } -) - -func usage() { - fmt.Printf("Usage: %s [OPTIONS] \n\n", os.Args[0]) - fmt.Printf("Note: is ignored when -dir is set\n\nOPTIONS:\n") - flag.PrintDefaults() -} - -func main() { - go func() { - if r := recover(); r != nil { - fmt.Fprintln(os.Stderr, "E: ", r) - os.Exit(-1) - } - }() - flag.Usage = usage - dir := flag.String("dir", "", "Directory to analyse") - recursive := flag.Bool("r", false, "Analyzes the directory recursively (Require -dir)") - jpegQuality := flag.Int("q", jpeg.DefaultQuality, "Define the jpeg quality") - ignoreErr := flag.Bool("ignore-errors", false, "Continue to convert when the converter encounter an error") - removeOrig := flag.Bool("remove-original", false, "Remove the heic file after conversion") - outPath := flag.String("o", "", "Output directory") - flag.Parse() - - if *jpegQuality < 1 || *jpegQuality > 100 { - fmt.Println("W: Illegal value for jpeg quality, quality is set to ", jpeg.DefaultQuality) - *jpegQuality = jpeg.DefaultQuality - } - - var files []string - if len(*dir) == 0 { - if len(os.Args) == 1 { - fmt.Fprintln(os.Stderr, "E: Missing file path") - os.Exit(-1) - } - files = append(files, os.Args[len(os.Args)-1]) - } else { - var err error - files, err = getHEICFiles(*dir, *recursive) - if err != nil { - fmt.Fprintf(os.Stderr, "E: Failed to analyse directory: %v\n", err) - os.Exit(-1) - } - } - if len(files) == 0 { - fmt.Println("No heic file found") - return - } - - fmt.Printf("%d files found with the extension .heic\n\n", len(files)) - - if *removeOrig { - fmt.Printf("WARNING: all the .heic will be deleted after the convertion\n\n") - } - - if askForConfirmation("Do you want to start the conversion?") { - out := *dir - overwritePath := false - if len(*outPath) > 0 { - out = *outPath - overwritePath = true - } - start := time.Now() - converted := convert(out, files, *ignoreErr, *jpegQuality, overwritePath) - - if *removeOrig { - for _, file := range converted { - err := os.Remove(file) - if err != nil { - fmt.Fprintf(os.Stderr, "E: Failed to remove %s: %v\n", file, err) - } - } - } - - fmt.Printf("%d file(s) converted in %v\n", len(converted), time.Since(start)) - } -} - -func getHEICFiles(dir string, rec bool) ([]string, error) { - var res []string - i, err := os.Stat(dir) - if err != nil { - return nil, err - } - if !i.IsDir() { - return nil, fmt.Errorf("%s is not a directory", dir) - } - if rec { - err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { - if isHEIC(info) { - res = append(res, path) - } - return nil - }) - if err != nil { - return nil, err - } - return res, nil - } - entries, err := os.ReadDir(dir) - if err != nil { - return nil, err - } - for _, info := range entries { - if isHEIC(info) { - res = append(res, filepath.Join(dir, info.Name())) - } - } - return res, nil -} - -func isHEIC(f filer) bool { - if f == nil { - return false - } - return !f.IsDir() && strings.HasSuffix(f.Name(), ".heic") -} - -func convert(out string, files []string, ignoreErr bool, jpegQuality int, overwritePath bool) []string { - var success []string - wg := new(sync.WaitGroup) - chunk := chunkWork(files) - for _, files := range chunk { - wg.Add(1) - go func(files []string, wg *sync.WaitGroup) { - for _, file := range files { - fi, err := os.Open(file) - if err != nil { - fmt.Fprintf(os.Stderr, "E: Failed to open %s: %v\n", file, err) - if ignoreErr { - continue - } - os.Exit(-1) - } - - ex, err := goheif.ExtractExif(fi) - if err != nil { - fmt.Printf("W: no EXIF from %s: %v\n", file, err) - } - - img, err := goheif.Decode(fi) - if err != nil { - fi.Close() - fmt.Fprintf(os.Stderr, "E: Failed to decode %s: %v\n", file, err) - if ignoreErr { - continue - } - os.Exit(-1) - } - - fout := strings.TrimSuffix(file, ".heic") + ".jpg" - if overwritePath { - fout = filepath.Join(out, filepath.Base(fout)) - } - - fo, err := os.OpenFile(fout, os.O_RDWR|os.O_CREATE, 0644) - if err != nil { - fi.Close() - fmt.Fprintf(os.Stderr, "E: Failed to create output file %s: %v\n", fout, err) - if ignoreErr { - continue - } - os.Exit(-1) - } - - w, _ := exif.NewWriterExif(fo, ex) - err = jpeg.Encode(w, img, &jpeg.Options{ - Quality: jpegQuality, - }) - if err != nil { - fi.Close() - fo.Close() - fmt.Fprintf(os.Stderr, "E: Failed to encode %s: %v\n", fout, err) - if ignoreErr { - continue - } - os.Exit(-1) - } - fi.Close() - fo.Close() - success = append(success, file) - } - wg.Done() - }(files, wg) - } - wg.Wait() - return success -} - -func chunkWork(files []string) [][]string { - var divided [][]string - chunkSize := (len(files) + runtime.NumCPU() - 1) / runtime.NumCPU() - for i := 0; i < len(files); i += chunkSize { - end := i + chunkSize - - if end > len(files) { - end = len(files) - } - - divided = append(divided, files[i:end]) - } - return divided -} - -func askForConfirmation(s string) bool { - reader := bufio.NewReader(os.Stdin) - - for { - fmt.Printf("%s [y/n]: ", s) - - response, err := reader.ReadString('\n') - if err != nil { - panic(err) - } - - response = strings.ToLower(strings.TrimSpace(response)) - - if response == "y" || response == "yes" { - return true - } else if response == "n" || response == "no" { - return false - } - } -} +package main + +import ( + "bufio" + "flag" + "fmt" + "image/jpeg" + "io/fs" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "time" + + "github.com/adrium/goheif" + "github.com/mojitaurelie/heic2jpg/exif" +) + +type ( + filer interface { + IsDir() bool + Name() string + } +) + +func usage() { + fmt.Printf("Usage: %s [OPTIONS] \n\n", os.Args[0]) + fmt.Printf("Note: is ignored when -dir is set\n\nOPTIONS:\n") + flag.PrintDefaults() +} + +func main() { + go func() { + if r := recover(); r != nil { + _, err := fmt.Fprintln(os.Stderr, "E: ", r) + if err != nil { + os.Exit(-255) + } + os.Exit(-1) + } + }() + flag.Usage = usage + dir := flag.String("dir", "", "Directory to analyse") + recursive := flag.Bool("r", false, "Analyzes the directory recursively (Require -dir)") + jpegQuality := flag.Int("q", jpeg.DefaultQuality, "Define the jpeg quality") + ignoreErr := flag.Bool("ignore-errors", false, "Continue to convert when the converter encounter an error") + removeOrig := flag.Bool("remove-original", false, "Remove the heic file after conversion") + overwriteOut := flag.Bool("overwrite", false, "Overwrite file if already exists") + outPath := flag.String("o", "", "Output directory") + flag.Parse() + + if *jpegQuality < 1 || *jpegQuality > 100 { + fmt.Println("W: Illegal value for jpeg quality, quality is set to ", jpeg.DefaultQuality) + *jpegQuality = jpeg.DefaultQuality + } + + var files []string + if len(*dir) == 0 { + if len(os.Args) == 1 { + _, err := fmt.Fprintln(os.Stderr, "E: Missing file path") + if err != nil { + panic(err) + } + os.Exit(-1) + } + files = append(files, os.Args[len(os.Args)-1]) + } else { + fmt.Printf("Analyzing folder %s\n", *dir) + var err error + files, err = getHEICFiles(*dir, *recursive) + if err != nil { + _, err := fmt.Fprintf(os.Stderr, "E: Failed to analyse directory: %v\n", err) + if err != nil { + panic(err) + } + os.Exit(-1) + } + } + if len(files) == 0 { + fmt.Println("No heic file found") + return + } + + fmt.Printf("%d files found with the extension .heic\n\n", len(files)) + + if *removeOrig { + fmt.Printf("WARNING: all the .heic will be deleted after the convertion\n\n") + } + + if askForConfirmation("Do you want to start the conversion?") { + out := *dir + rewritePath := false + if len(*outPath) > 0 { + out = *outPath + rewritePath = true + } + start := time.Now() + converted := convert(out, files, *ignoreErr, *jpegQuality, rewritePath, *overwriteOut) + + if *removeOrig { + for _, file := range converted { + err := os.Remove(file) + if err != nil { + _, err := fmt.Fprintf(os.Stderr, "E: Failed to remove %s: %v\n", file, err) + if err != nil { + panic(err) + } + } + } + } + + fmt.Printf("%d file(s) converted in %v\n", len(converted), time.Since(start)) + } +} + +func getHEICFiles(dir string, rec bool) ([]string, error) { + var res []string + i, err := os.Stat(dir) + if err != nil { + return nil, err + } + if !i.IsDir() { + return nil, fmt.Errorf("%s is not a directory", dir) + } + if rec { + err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { + if isHEIC(info) { + res = append(res, path) + } + return nil + }) + if err != nil { + return nil, err + } + return res, nil + } + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + for _, info := range entries { + if isHEIC(info) { + res = append(res, filepath.Join(dir, info.Name())) + } + } + return res, nil +} + +func isHEIC(f filer) bool { + if f == nil { + return false + } + return !f.IsDir() && strings.HasSuffix(f.Name(), ".heic") +} + +func convert(out string, files []string, ignoreErr bool, jpegQuality int, rewritePath, overwrite bool) []string { + var success []string + wg := new(sync.WaitGroup) + chunk := chunkWork(files) + for _, files := range chunk { + wg.Add(1) + go func(files []string, wg *sync.WaitGroup) { + for _, file := range files { + // Generate new path + fout := strings.TrimSuffix(file, ".heic") + ".jpg" + if rewritePath { + fout = filepath.Join(out, filepath.Base(fout)) + } + + // Ignore if file exists and overwirte arg not true + if !overwrite { + if _, err := os.Stat(fout); err == nil { + continue + } + } + + // Open source file + fi, err := os.Open(file) + if err != nil { + _, err := fmt.Fprintf(os.Stderr, "E: Failed to open %s: %v\n", file, err) + if err != nil { + panic(err) + } + if ignoreErr { + continue + } + os.Exit(-1) + } + + // Extract exif + ex, err := goheif.ExtractExif(fi) + if err != nil { + fmt.Printf("W: no EXIF from %s: %v\n", file, err) + } + + // Parse jpg to bitmap + img, err := goheif.Decode(fi) + if err != nil { + err := fi.Close() + if err != nil { + panic(err) + } + _, err = fmt.Fprintf(os.Stderr, "E: Failed to decode %s: %v\n", file, err) + if err != nil { + panic(err) + } + if ignoreErr { + continue + } + os.Exit(-1) + } + + // Open out file + fo, err := os.OpenFile(fout, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + err := fi.Close() + if err != nil { + panic(err) + } + _, err = fmt.Fprintf(os.Stderr, "E: Failed to create output file %s: %v\n", fout, err) + if err != nil { + panic(err) + } + if ignoreErr { + continue + } + os.Exit(-1) + } + + // Convert and write to jpg file + w, _ := exif.NewWriterExif(fo, ex) + err = jpeg.Encode(w, img, &jpeg.Options{ + Quality: jpegQuality, + }) + if err != nil { + err := fi.Close() + if err != nil { + panic(err) + } + err = fo.Close() + if err != nil { + panic(err) + } + _, err = fmt.Fprintf(os.Stderr, "E: Failed to encode %s: %v\n", fout, err) + if err != nil { + panic(err) + } + if ignoreErr { + continue + } + os.Exit(-1) + } + err = fi.Close() + if err != nil { + panic(err) + } + err = fo.Close() + if err != nil { + panic(err) + } + success = append(success, file) + } + wg.Done() + }(files, wg) + } + wg.Wait() + return success +} + +func chunkWork(files []string) [][]string { + var divided [][]string + chunkSize := (len(files) + runtime.NumCPU() - 1) / runtime.NumCPU() + for i := 0; i < len(files); i += chunkSize { + end := i + chunkSize + + if end > len(files) { + end = len(files) + } + + divided = append(divided, files[i:end]) + } + return divided +} + +func askForConfirmation(s string) bool { + reader := bufio.NewReader(os.Stdin) + fmt.Printf("%s [y/N]: ", s) + + response, err := reader.ReadString('\n') + if err != nil { + panic(err) + } + + response = strings.ToLower(strings.TrimSpace(response)) + + if response == "y" || response == "yes" { + return true + } + return false +}