first commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.exe
|
||||
55
exif/exif.go
Normal file
55
exif/exif.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package exif
|
||||
|
||||
import "io"
|
||||
|
||||
type (
|
||||
writerSkipper struct {
|
||||
io.Writer
|
||||
bytesToSkip int
|
||||
}
|
||||
)
|
||||
|
||||
func NewWriterExif(w io.Writer, exif []byte) (io.Writer, error) {
|
||||
writer := &writerSkipper{
|
||||
Writer: w,
|
||||
bytesToSkip: 2,
|
||||
}
|
||||
soi := []byte{0xff, 0xd8}
|
||||
if _, err := w.Write(soi); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if exif != nil {
|
||||
app1Marker := 0xe1
|
||||
markerlen := 2 + len(exif)
|
||||
marker := []byte{0xff, uint8(app1Marker), uint8(markerlen >> 8), uint8(markerlen & 0xff)}
|
||||
if _, err := w.Write(marker); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := w.Write(exif); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return writer, nil
|
||||
}
|
||||
|
||||
func (w *writerSkipper) Write(data []byte) (int, error) {
|
||||
if w.bytesToSkip <= 0 {
|
||||
return w.Writer.Write(data)
|
||||
}
|
||||
|
||||
if dataLen := len(data); dataLen < w.bytesToSkip {
|
||||
w.bytesToSkip -= dataLen
|
||||
return dataLen, nil
|
||||
}
|
||||
|
||||
if n, err := w.Writer.Write(data[w.bytesToSkip:]); err == nil {
|
||||
n += w.bytesToSkip
|
||||
w.bytesToSkip = 0
|
||||
return n, nil
|
||||
} else {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
7
go.mod
Normal file
7
go.mod
Normal file
@@ -0,0 +1,7 @@
|
||||
module github.com/mojitaurelie/heic2jpg
|
||||
|
||||
go 1.20
|
||||
|
||||
require github.com/adrium/goheif v0.0.0-20230113233934-ca402e77a786
|
||||
|
||||
require github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
|
||||
4
go.sum
Normal file
4
go.sum
Normal file
@@ -0,0 +1,4 @@
|
||||
github.com/adrium/goheif v0.0.0-20230113233934-ca402e77a786 h1:zvgtcRb2B5gynWjm+Fc9oJZPHXwmcgyH0xCcNm6Rmo4=
|
||||
github.com/adrium/goheif v0.0.0-20230113233934-ca402e77a786/go.mod h1:aKVJoQ0cc9K5Xb058XSnnAxXLliR97qbSqWBlm5ca1E=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
248
main.go
Normal file
248
main.go
Normal file
@@ -0,0 +1,248 @@
|
||||
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] <file>\n\n", os.Args[0])
|
||||
fmt.Printf("Note: <file> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user