diff --git a/carver.go b/carver.go index b084ddaf..d45dad35 100644 --- a/carver.go +++ b/carver.go @@ -77,7 +77,7 @@ func (c *Carver) ComputeSeams(p *Processor, img *image.NRGBA) (*image.NRGBA, err dets := []pigo.Detection{} - if p.PigoFaceDetector != nil && p.FaceDetect && detAttempts < maxFaceDetAttempts { + if p.FaceDetector != nil && p.FaceDetect && detAttempts < maxFaceDetAttempts { var ratio float64 if width < height { @@ -108,10 +108,10 @@ func (c *Carver) ComputeSeams(p *Processor, img *image.NRGBA) (*image.NRGBA, err } // Run the classifier over the obtained leaf nodes and return the detection results. // The result contains quadruplets representing the row, column, scale and detection score. - dets = p.PigoFaceDetector.RunCascade(cParams, p.FaceAngle) + dets = p.FaceDetector.RunCascade(cParams, p.FaceAngle) // Calculate the intersection over union (IoU) of two clusters. - dets = p.PigoFaceDetector.ClusterDetections(dets, 0.1) + dets = p.FaceDetector.ClusterDetections(dets, 0.1) if len(dets) == 0 { // Retry detecting faces for a certain amount of time. diff --git a/carver_test.go b/carver_test.go index bb21b50f..ec6df451 100644 --- a/carver_test.go +++ b/carver_test.go @@ -250,7 +250,7 @@ func TestCarver_ShouldDetectFace(t *testing.T) { } defer f.Close() - p.PigoFaceDetector, err = p.PigoFaceDetector.Unpack(cascadeFile) + p.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile) if err != nil { t.Fatalf("error unpacking the cascade file: %v", err) } @@ -282,10 +282,10 @@ func TestCarver_ShouldDetectFace(t *testing.T) { // Run the classifier over the obtained leaf nodes and return the detection results. // The result contains quadruplets representing the row, column, scale and detection score. - faces := p.PigoFaceDetector.RunCascade(cParams, p.FaceAngle) + faces := p.FaceDetector.RunCascade(cParams, p.FaceAngle) // Calculate the intersection over union (IoU) of two clusters. - faces = p.PigoFaceDetector.ClusterDetections(faces, 0.2) + faces = p.FaceDetector.ClusterDetections(faces, 0.2) assert.Equal(1, len(faces)) } @@ -301,7 +301,7 @@ func TestCarver_ShouldNotRemoveFaceZone(t *testing.T) { } defer f.Close() - p.PigoFaceDetector, err = p.PigoFaceDetector.Unpack(cascadeFile) + p.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile) if err != nil { t.Fatalf("error unpacking the cascade file: %v", err) } @@ -336,10 +336,10 @@ func TestCarver_ShouldNotRemoveFaceZone(t *testing.T) { // Run the classifier over the obtained leaf nodes and return the detection results. // The result contains quadruplets representing the row, column, scale and detection score. - faces := p.PigoFaceDetector.RunCascade(cParams, p.FaceAngle) + faces := p.FaceDetector.RunCascade(cParams, p.FaceAngle) // Calculate the intersection over union (IoU) of two clusters. - faces = p.PigoFaceDetector.ClusterDetections(faces, 0.2) + faces = p.FaceDetector.ClusterDetections(faces, 0.2) // Range over all the detected faces and draw a white rectangle mask over each of them. // We need to trick the sobel detector to consider them as important image parts. @@ -378,7 +378,7 @@ func TestCarver_ShouldNotResizeWithFaceDistorsion(t *testing.T) { } defer f.Close() - p.PigoFaceDetector, err = p.PigoFaceDetector.Unpack(cascadeFile) + p.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile) if err != nil { t.Fatalf("error unpacking the cascade file: %v", err) } @@ -409,10 +409,10 @@ func TestCarver_ShouldNotResizeWithFaceDistorsion(t *testing.T) { // Run the classifier over the obtained leaf nodes and return the detection results. // The result contains quadruplets representing the row, column, scale and detection score. - faces := p.PigoFaceDetector.RunCascade(cParams, p.FaceAngle) + faces := p.FaceDetector.RunCascade(cParams, p.FaceAngle) // Calculate the intersection over union (IoU) of two clusters. - faces = p.PigoFaceDetector.ClusterDetections(faces, 0.2) + faces = p.FaceDetector.ClusterDetections(faces, 0.2) for _, face := range faces { if p.NewHeight < face.Scale { diff --git a/cmd/caire/main.go b/cmd/caire/main.go index 0d247410..d3bd6566 100644 --- a/cmd/caire/main.go +++ b/cmd/caire/main.go @@ -1,23 +1,15 @@ package main import ( - "errors" "flag" "fmt" - "io" "log" "os" - "os/signal" - "path/filepath" "runtime" - "sync" - "syscall" - "time" "gioui.org/app" "github.com/esimov/caire" "github.com/esimov/caire/utils" - "golang.org/x/term" ) const HelpBanner = ` @@ -33,22 +25,6 @@ Content aware image resize library. // pipeName indicates that stdin/stdout is being used as file names. const pipeName = "-" -// maxWorkers sets the maximum number of concurrently running workers. -const maxWorkers = 20 - -// result holds the relevant information about the resizing process and the generated image. -type result struct { - path string - err error -} - -var ( - // imgfile holds the file being accessed, be it normal file or pipe name. - imgfile *os.File - // spinner is used to instantiate and call the progress indicator. - spinner *utils.Spinner -) - // Version indicates the current build version. var Version string @@ -71,13 +47,11 @@ var ( faceDetect = flag.Bool("face", false, "Use face detection") faceAngle = flag.Float64("angle", 0.0, "Face rotation angle") workers = flag.Int("conc", runtime.NumCPU(), "Number of files to process concurrently") - - // Common file related variable - fs os.FileInfo ) func main() { log.SetFlags(0) + flag.Usage = func() { fmt.Fprintf(os.Stderr, fmt.Sprintf(HelpBanner, Version)) flag.PrintDefaults() @@ -101,13 +75,6 @@ func main() { SeamColor: *seamColor, } - defaultMsg := fmt.Sprintf("%s %s", - utils.DecorateText("⚡ CAIRE", utils.StatusMessage), - utils.DecorateText("⇢ image resizing in progress (be patient, it may take a while)...", utils.DefaultMessage), - ) - - spinner = utils.NewSpinner(defaultMsg, time.Millisecond*80) - if !(*newWidth > 0 || *newHeight > 0 || *percentage || *square) { flag.Usage() log.Fatal(fmt.Sprintf("%s%s", @@ -115,335 +82,21 @@ func main() { utils.DefaultColor, )) } else { + op := &caire.Ops{ + Src: *source, + Dst: *destination, + Workers: *workers, + PipeName: pipeName, + } + if *preview { - // When the preview mode is activated we need to execute the resizing process + // When the preview mode is activated we have to execute the resizing process // in a separate goroutine in order to not block the Gio thread, - // which needs to be run on the main OS thread on operating systems like MacOS. - go execute(proc) + // which have to run on the main OS thread of the operating systems like MacOS. + go proc.Execute(op) app.Main() } else { - execute(proc) - } - } -} - -// execute executes the image resizing process. -// In case the preview mode is activated it will be invoked in a separate goroutine -// in order to not block the main OS thread. Otherwise it will be called normally. -func execute(proc *caire.Processor) { - var err error - proc.Spinner = spinner - - // Supported files - validExtensions := []string{".jpg", ".png", ".jpeg", ".bmp", ".gif"} - - // Check if source path is a local image or URL. - if utils.IsValidUrl(*source) { - src, err := utils.DownloadImage(*source) - if src != nil { - defer os.Remove(src.Name()) - } - defer src.Close() - if err != nil { - log.Fatalf( - utils.DecorateText("Failed to load the source image: %v", utils.ErrorMessage), - utils.DecorateText(err.Error(), utils.DefaultMessage), - ) - } - fs, err = src.Stat() - if err != nil { - log.Fatalf( - utils.DecorateText("Failed to load the source image: %v", utils.ErrorMessage), - utils.DecorateText(err.Error(), utils.DefaultMessage), - ) - } - img, err := os.Open(src.Name()) - if err != nil { - log.Fatalf( - utils.DecorateText("Unable to open the temporary image file: %v", utils.ErrorMessage), - utils.DecorateText(err.Error(), utils.DefaultMessage), - ) - } - imgfile = img - } else { - // Check if the source is a pipe name or a regular file. - if *source == pipeName { - fs, err = os.Stdin.Stat() - } else { - fs, err = os.Stat(*source) - } - if err != nil { - log.Fatalf( - utils.DecorateText("Failed to load the source image: %v", utils.ErrorMessage), - utils.DecorateText(err.Error(), utils.DefaultMessage), - ) - } - } - - now := time.Now() - - switch mode := fs.Mode(); { - case mode.IsDir(): - var wg sync.WaitGroup - // Read destination file or directory. - _, err := os.Stat(*destination) - if err != nil { - err = os.Mkdir(*destination, 0755) - if err != nil { - log.Fatalf( - utils.DecorateText("Unable to get dir stats: %v\n", utils.ErrorMessage), - utils.DecorateText(err.Error(), utils.DefaultMessage), - ) - } - } - proc.Preview = false - - // Limit the concurrently running workers to maxWorkers. - if *workers <= 0 || *workers > maxWorkers { - *workers = runtime.NumCPU() - } - - // Process recursively the image files from the specified directory concurrently. - ch := make(chan result) - done := make(chan interface{}) - defer close(done) - - paths, errc := walkDir(done, *source, validExtensions) - - wg.Add(*workers) - for i := 0; i < *workers; i++ { - go func() { - defer wg.Done() - consumer(done, paths, *destination, proc, ch) - }() - } - - // Close the channel after the values are consumed. - go func() { - defer close(ch) - wg.Wait() - }() - - // Consume the channel values. - for res := range ch { - if res.err != nil { - err = res.err - } - printStatus(res.path, err) - } - - if err = <-errc; err != nil { - fmt.Fprintf(os.Stderr, utils.DecorateText(err.Error(), utils.ErrorMessage)) - } - - case mode.IsRegular() || mode&os.ModeNamedPipe != 0: // check for regular files or pipe names - ext := filepath.Ext(*destination) - if !isValidExtension(ext, validExtensions) && *destination != pipeName { - log.Fatalf(utils.DecorateText(fmt.Sprintf("%v file type not supported", ext), utils.ErrorMessage)) - } - - err = processor(*source, *destination, proc) - printStatus(*destination, err) - } - if err == nil { - fmt.Fprintf(os.Stderr, "\nExecution time: %s\n", utils.DecorateText(fmt.Sprintf("%s", utils.FormatTime(time.Since(now))), utils.SuccessMessage)) - } -} - -// walkDir starts a goroutine to walk the specified directory tree in recursive manner -// and send the path of each regular file on the string channel. -// It terminates in case done channel is closed. -func walkDir( - done <-chan interface{}, - src string, - srcExts []string, -) (<-chan string, <-chan error) { - pathChan := make(chan string) - errChan := make(chan error, 1) - - go func() { - // Close the paths channel after Walk returns. - defer close(pathChan) - - errChan <- filepath.Walk(src, func(path string, f os.FileInfo, err error) error { - isFileSupported := false - if err != nil { - return err - } - if !f.Mode().IsRegular() { - return nil - } - - // Get the file base name. - fx := filepath.Ext(f.Name()) - for _, ext := range srcExts { - if ext == fx { - isFileSupported = true - break - } - } - - if isFileSupported { - select { - case <-done: - return errors.New("directory walk cancelled") - case pathChan <- path: - } - } - return nil - }) - }() - return pathChan, errChan -} - -// consumer reads the path names from the paths channel and calls the resizing processor against the source image. -func consumer( - done <-chan interface{}, - paths <-chan string, - dest string, - proc *caire.Processor, - res chan<- result, -) { - for src := range paths { - dst := filepath.Join(dest, filepath.Base(src)) - err := processor(src, dst, proc) - - select { - case <-done: - return - case res <- result{ - path: src, - err: err, - }: - } - } -} - -// processor calls the resizer method over the source image and returns the error in case exists. -func processor(in, out string, proc *caire.Processor) error { - var ( - successMsg string - errorMsg string - ) - // Start the progress indicator. - spinner.Start() - - successMsg = fmt.Sprintf("%s %s %s", - utils.DecorateText("⚡ CAIRE", utils.StatusMessage), - utils.DecorateText("⇢", utils.DefaultMessage), - utils.DecorateText("the image has been resized successfully ✔", utils.SuccessMessage), - ) - - errorMsg = fmt.Sprintf("%s %s %s", - utils.DecorateText("⚡ CAIRE", utils.StatusMessage), - utils.DecorateText("resizing image failed...", utils.DefaultMessage), - utils.DecorateText("✘", utils.ErrorMessage), - ) - - src, dst, err := pathToFile(in, out) - if err != nil { - spinner.StopMsg = errorMsg - return err - } - - // Capture CTRL-C signal and restores back the cursor visibility. - signalChan := make(chan os.Signal, 1) - signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) - go func() { - <-signalChan - func() { - spinner.RestoreCursor() - os.Remove(dst.(*os.File).Name()) - os.Exit(1) - }() - }() - - defer src.(*os.File).Close() - defer dst.(*os.File).Close() - - err = proc.Process(src, dst) - if err != nil { - // remove the generated image file in case of an error - os.Remove(dst.(*os.File).Name()) - - spinner.StopMsg = errorMsg - // Stop the progress indicator. - spinner.Stop() - - return err - } else { - spinner.StopMsg = successMsg - // Stop the progress indicator. - spinner.Stop() - } - - return nil -} - -// pathToFile converts the source and destination paths to readable and writable files. -func pathToFile(in, out string) (io.Reader, io.Writer, error) { - var ( - src io.Reader - dst io.Writer - err error - ) - // Check if the source path is a local image or URL. - if utils.IsValidUrl(in) { - src = imgfile - } else { - // Check if the source is a pipe name or a regular file. - if in == pipeName { - if term.IsTerminal(int(os.Stdin.Fd())) { - return nil, nil, errors.New("`-` should be used with a pipe for stdin") - } - src = os.Stdin - } else { - src, err = os.Open(in) - if err != nil { - return nil, nil, fmt.Errorf("unable to open the source file: %v", err) - } - } - } - - // Check if the destination is a pipe name or a regular file. - if out == pipeName { - if term.IsTerminal(int(os.Stdout.Fd())) { - return nil, nil, errors.New("`-` should be used with a pipe for stdout") - } - dst = os.Stdout - } else { - dst, err = os.OpenFile(out, os.O_CREATE|os.O_WRONLY, 0755) - if err != nil { - return nil, nil, fmt.Errorf("unable to create the destination file: %v", err) - } - } - return src, dst, nil -} - -// printStatus displays the relavant information about the image resizing process. -func printStatus(fname string, err error) { - if err != nil { - fmt.Fprintf(os.Stderr, - utils.DecorateText("\nError resizing the image: %s", utils.ErrorMessage), - utils.DecorateText(fmt.Sprintf("\n\tReason: %v\n", err.Error()), utils.DefaultMessage), - ) - os.Exit(0) - } else { - if fname != pipeName { - fmt.Fprintf(os.Stderr, "\nThe image has been saved as: %s %s\n\n", - utils.DecorateText(filepath.Base(fname), utils.SuccessMessage), - utils.DefaultColor, - ) - } - } -} - -// isValidExtension checks for the supported extensions. -func isValidExtension(ext string, extensions []string) bool { - for _, ex := range extensions { - if ex == ext { - return true + proc.Execute(op) } } - return false } diff --git a/exec.go b/exec.go new file mode 100644 index 00000000..f4b360b0 --- /dev/null +++ b/exec.go @@ -0,0 +1,378 @@ +package caire + +import ( + "errors" + "fmt" + "io" + "log" + "os" + "os/signal" + "path/filepath" + "runtime" + "sync" + "syscall" + "time" + + "github.com/esimov/caire/utils" + "golang.org/x/term" +) + +// maxWorkers sets the maximum number of concurrently running workers. +const maxWorkers = 20 + +var ( + // imgFile holds the file being accessed, be it normal file or pipe name. + imgFile *os.File + + // Common file related variable + fs os.FileInfo +) + +type Ops struct { + Src, Dst, PipeName string + Workers int +} + +// result holds the relevant information about the resizing process and the generated image. +type result struct { + path string + err error +} + +// Execute executes the image resizing process. +// In case the preview mode is activated it will be invoked in a separate goroutine +// in order to not block the main OS thread. Otherwise it will be called normally. +func (p *Processor) Execute(op *Ops) { + var err error + defaultMsg := fmt.Sprintf("%s %s", + utils.DecorateText("⚡ CAIRE", utils.StatusMessage), + utils.DecorateText("⇢ resizing image (be patient, it may take a while)...", utils.DefaultMessage), + ) + p.Spinner = utils.NewSpinner(defaultMsg, time.Millisecond*80) + + // Supported files + validExtensions := []string{".jpg", ".png", ".jpeg", ".bmp", ".gif"} + + // Check if source path is a local image or URL. + if utils.IsValidUrl(op.Src) { + src, err := utils.DownloadImage(op.Src) + if src != nil { + defer os.Remove(src.Name()) + } + + if err != nil { + log.Fatalf( + utils.DecorateText("Failed to load the source image: %v", utils.ErrorMessage), + utils.DecorateText(err.Error(), utils.DefaultMessage), + ) + } + fs, err = src.Stat() + if err != nil { + log.Fatalf( + utils.DecorateText("Failed to load the source image: %v", utils.ErrorMessage), + utils.DecorateText(err.Error(), utils.DefaultMessage), + ) + } + img, err := os.Open(src.Name()) + if err != nil { + log.Fatalf( + utils.DecorateText("Unable to open the temporary image file: %v", utils.ErrorMessage), + utils.DecorateText(err.Error(), utils.DefaultMessage), + ) + } + + imgFile = img + } else { + // Check if the source is a pipe name or a regular file. + if op.Src == op.PipeName { + fs, err = os.Stdin.Stat() + } else { + fs, err = os.Stat(op.Src) + } + if err != nil { + log.Fatalf( + utils.DecorateText("Failed to load the source image: %v", utils.ErrorMessage), + utils.DecorateText(err.Error(), utils.DefaultMessage), + ) + } + } + + now := time.Now() + + switch mode := fs.Mode(); { + case mode.IsDir(): + var wg sync.WaitGroup + // Read destination file or directory. + _, err := os.Stat(op.Dst) + if err != nil { + err = os.Mkdir(op.Dst, 0755) + if err != nil { + log.Fatalf( + utils.DecorateText("Unable to get dir stats: %v\n", utils.ErrorMessage), + utils.DecorateText(err.Error(), utils.DefaultMessage), + ) + } + } + p.Preview = false + + // Limit the concurrently running workers to maxWorkers. + if op.Workers <= 0 || op.Workers > maxWorkers { + op.Workers = runtime.NumCPU() + } + + // Process recursively the image files from the specified directory concurrently. + ch := make(chan result) + done := make(chan interface{}) + defer close(done) + + paths, errc := walkDir(done, op.Src, validExtensions) + + wg.Add(op.Workers) + for i := 0; i < op.Workers; i++ { + go func() { + defer wg.Done() + op.consumer(p, op.Dst, ch, done, paths) + }() + } + + // Close the channel after the values are consumed. + go func() { + defer close(ch) + wg.Wait() + }() + + // Consume the channel values. + for res := range ch { + if res.err != nil { + err = res.err + } + op.printOpStatus(res.path, err) + } + + if err = <-errc; err != nil { + fmt.Fprintf(os.Stderr, utils.DecorateText(err.Error(), utils.ErrorMessage)) + } + + case mode.IsRegular() || mode&os.ModeNamedPipe != 0: // check for regular files or pipe names + ext := filepath.Ext(op.Dst) + if !isValidExtension(ext, validExtensions) && op.Dst != op.PipeName { + log.Fatalf(utils.DecorateText(fmt.Sprintf("%v file type not supported", ext), utils.ErrorMessage)) + } + + err = op.process(p, op.Src, op.Dst) + op.printOpStatus(op.Dst, err) + } + if err == nil { + fmt.Fprintf(os.Stderr, "\nExecution time: %s\n", utils.DecorateText(fmt.Sprintf("%s", utils.FormatTime(time.Since(now))), utils.SuccessMessage)) + } +} + +// consumer reads the path names from the paths channel and calls the resizing processor against the source image. +func (op *Ops) consumer( + p *Processor, + dest string, + res chan<- result, + done <-chan interface{}, + paths <-chan string, +) { + for src := range paths { + dst := filepath.Join(dest, filepath.Base(src)) + err := op.process(p, src, dst) + + select { + case <-done: + return + case res <- result{ + path: src, + err: err, + }: + } + } +} + +// processor calls the resizer method over the source image and returns the error in case exists. +func (op *Ops) process(p *Processor, in, out string) error { + var ( + successMsg string + errorMsg string + ) + // Start the progress indicator. + p.Spinner.Start() + + successMsg = fmt.Sprintf("%s %s %s", + utils.DecorateText("⚡ CAIRE", utils.StatusMessage), + utils.DecorateText("⇢", utils.DefaultMessage), + utils.DecorateText("the image has been resized successfully ✔", utils.SuccessMessage), + ) + + errorMsg = fmt.Sprintf("%s %s %s", + utils.DecorateText("⚡ CAIRE", utils.StatusMessage), + utils.DecorateText("resizing image failed...", utils.DefaultMessage), + utils.DecorateText("✘", utils.ErrorMessage), + ) + + src, dst, err := op.pathToFile(in, out) + if err != nil { + p.Spinner.StopMsg = errorMsg + return err + } + + // Capture CTRL-C signal and restores back the cursor visibility. + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) + go func() { + <-signalChan + func() { + p.Spinner.RestoreCursor() + os.Remove(dst.(*os.File).Name()) + os.Exit(1) + }() + }() + + defer func() { + if img, ok := src.(*os.File); ok { + if err := img.Close(); err != nil { + log.Printf("could not close the opened file: %v", err) + } + } + }() + + defer func() { + if img, ok := dst.(*os.File); ok { + if err := img.Close(); err != nil { + log.Printf("could not close the opened file: %v", err) + } + } + }() + + err = p.Process(src, dst) + if err != nil { + // remove the generated image file in case of an error + os.Remove(dst.(*os.File).Name()) + + p.Spinner.StopMsg = errorMsg + // Stop the progress indicator. + p.Spinner.Stop() + + return err + } else { + p.Spinner.StopMsg = successMsg + // Stop the progress indicator. + p.Spinner.Stop() + } + + return nil +} + +// pathToFile converts the source and destination paths to readable and writable files. +func (op *Ops) pathToFile(in, out string) (io.Reader, io.Writer, error) { + var ( + src io.Reader + dst io.Writer + err error + ) + // Check if the source path is a local image or URL. + if utils.IsValidUrl(in) { + src = imgFile + } else { + // Check if the source is a pipe name or a regular file. + if in == op.PipeName { + if term.IsTerminal(int(os.Stdin.Fd())) { + return nil, nil, errors.New("`-` should be used with a pipe for stdin") + } + src = os.Stdin + } else { + src, err = os.Open(in) + if err != nil { + return nil, nil, fmt.Errorf("unable to open the source file: %v", err) + } + } + } + + // Check if the destination is a pipe name or a regular file. + if out == op.PipeName { + if term.IsTerminal(int(os.Stdout.Fd())) { + return nil, nil, errors.New("`-` should be used with a pipe for stdout") + } + dst = os.Stdout + } else { + dst, err = os.OpenFile(out, os.O_CREATE|os.O_WRONLY, 0755) + if err != nil { + return nil, nil, fmt.Errorf("unable to create the destination file: %v", err) + } + } + return src, dst, nil +} + +// printOpStatus displays the relevant information about the image resizing process. +func (op *Ops) printOpStatus(fname string, err error) { + if err != nil { + log.Fatalf( + utils.DecorateText("\nError resizing the image: %s", utils.ErrorMessage), + utils.DecorateText(fmt.Sprintf("\n\tReason: %v\n", err.Error()), utils.DefaultMessage), + ) + } else { + if fname != op.PipeName { + fmt.Fprintf(os.Stderr, "\nThe image has been saved as: %s %s\n\n", + utils.DecorateText(filepath.Base(fname), utils.SuccessMessage), + utils.DefaultColor, + ) + } + } +} + +// walkDir starts a new goroutine to walk the specified directory tree +// in recursive manner and sends the path of each regular file to a new channel. +// It finishes in case the done channel is getting closed. +func walkDir( + done <-chan interface{}, + src string, + srcExts []string, +) (<-chan string, <-chan error) { + pathChan := make(chan string) + errChan := make(chan error, 1) + + go func() { + // Close the paths channel after Walk returns. + defer close(pathChan) + + errChan <- filepath.Walk(src, func(path string, f os.FileInfo, err error) error { + isFileSupported := false + if err != nil { + return err + } + if !f.Mode().IsRegular() { + return nil + } + + // Get the file base name. + fx := filepath.Ext(f.Name()) + for _, ext := range srcExts { + if ext == fx { + isFileSupported = true + break + } + } + + if isFileSupported { + select { + case <-done: + return errors.New("directory walk cancelled") + case pathChan <- path: + } + } + return nil + }) + }() + return pathChan, errChan +} + +// isValidExtension checks for the supported extensions. +func isValidExtension(ext string, extensions []string) bool { + for _, ex := range extensions { + if ex == ext { + return true + } + } + return false +} diff --git a/go.mod b/go.mod index 59c29b81..18778067 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/esimov/caire -go 1.19 +go 1.22 require ( gioui.org v0.3.1 diff --git a/go.sum b/go.sum index 75b81e6a..ee18f41d 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY= +eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA= gioui.org v0.3.1 h1:hslYkrkIWvx28Mxe3A87opl+8s9mnWsnWmPDh11+zco= gioui.org v0.3.1/go.mod h1:2atiYR4upH71/6ehnh6XsUELa7JZOrOHHNMDxGBZF0Q= gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= @@ -17,6 +18,7 @@ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzP github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo= github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k= github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI= +github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/process.go b/processor.go similarity index 96% rename from process.go rename to processor.go index 6e9ed884..42841ff8 100644 --- a/process.go +++ b/processor.go @@ -61,25 +61,25 @@ type enlargeFn func(*Carver, *image.NRGBA) (*image.NRGBA, error) // Processor options type Processor struct { - SobelThreshold int - BlurRadius int - NewWidth int - NewHeight int - Percentage bool - Square bool - Debug bool - Preview bool - FaceDetect bool - ShapeType string - SeamColor string - MaskPath string - RMaskPath string - Mask *image.NRGBA - RMask *image.NRGBA - GuiDebug *image.NRGBA - FaceAngle float64 - PigoFaceDetector *pigo.Pigo - Spinner *utils.Spinner + SobelThreshold int + BlurRadius int + NewWidth int + NewHeight int + Percentage bool + Square bool + Debug bool + Preview bool + FaceDetect bool + ShapeType string + SeamColor string + MaskPath string + RMaskPath string + Mask *image.NRGBA + RMask *image.NRGBA + GuiDebug *image.NRGBA + FaceAngle float64 + FaceDetector *pigo.Pigo + Spinner *utils.Spinner vRes bool } @@ -476,13 +476,13 @@ func (p *Processor) calculateFitness(img *image.NRGBA, c *Carver) *image.NRGBA { func (p *Processor) Process(r io.Reader, w io.Writer) error { var err error - // Instantiate a new Pigo object in case the face detection option is used. - p.PigoFaceDetector = pigo.NewPigo() - if p.FaceDetect { + // Instantiate a new Pigo object in case the face detection option is used. + p.FaceDetector = pigo.NewPigo() + // Unpack the binary file. This will return the number of cascade trees, // the tree depth, the threshold and the prediction from tree's leaf nodes. - p.PigoFaceDetector, err = p.PigoFaceDetector.Unpack(cascadeFile) + p.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile) if err != nil { return fmt.Errorf("error unpacking the cascade file: %v", err) } @@ -494,6 +494,8 @@ func (p *Processor) Process(r io.Reader, w io.Writer) error { src, _, err := image.Decode(r) if err != nil { + fmt.Println("err:", err) + os.Exit(2) return err } diff --git a/process_test.go b/processor_test.go similarity index 100% rename from process_test.go rename to processor_test.go diff --git a/utils/download.go b/utils/download.go index a8785822..d1b00dbb 100644 --- a/utils/download.go +++ b/utils/download.go @@ -2,10 +2,9 @@ package utils import ( "bytes" - "errors" "fmt" "io" - "io/ioutil" + "log" "net/http" "net/url" "os" @@ -17,32 +16,35 @@ func DownloadImage(url string) (*os.File, error) { // Retrieve the url and decode the response body. res, err := http.Get(url) if err != nil { - return nil, errors.New(fmt.Sprintf("unable to download image file from URI: %s, status %v", url, res.Status)) + return nil, fmt.Errorf("unable to download image file from URI: %s, status %v", url, res.Status) } defer res.Body.Close() - data, err := ioutil.ReadAll(res.Body) + data, err := io.ReadAll(res.Body) if err != nil { - return nil, errors.New(fmt.Sprintf("unable to read response body: %s", err)) + return nil, fmt.Errorf("unable to read response body: %w", err) } - tmpfile, err := ioutil.TempFile("/tmp", "image") + tmpfile, err := os.CreateTemp("/tmp", "image") if err != nil { - return nil, errors.New(fmt.Sprintf("unable to create temporary file: %v", err)) + return nil, fmt.Errorf("unable to create temporary file: %w", err) } // Copy the image binary data into the temporary file. _, err = io.Copy(tmpfile, bytes.NewBuffer(data)) if err != nil { - return nil, errors.New(fmt.Sprintf("unable to copy the source URI into the destination file")) + return nil, fmt.Errorf("unable to copy the source URI into the destination file") } + ctype, err := DetectContentType(tmpfile.Name()) if err != nil { return nil, err } + if !strings.Contains(ctype.(string), "image") { - return nil, errors.New("the downloaded file is a valid image type.") + return nil, fmt.Errorf("the downloaded file is not a valid image type") } + return tmpfile, nil } @@ -67,7 +69,11 @@ func DetectContentType(fname string) (interface{}, error) { if err != nil { return nil, err } - defer file.Close() + defer func() { + if err := file.Close(); err != nil { + log.Printf("could not close the opened file: %v", err) + } + }() // Only the first 512 bytes are used to sniff the content type. buffer := make([]byte, 512)