Code refactoring and improvements

This commit is contained in:
esimov
2024-04-24 14:31:06 +03:00
parent d8bf889688
commit bdceadba96
9 changed files with 446 additions and 405 deletions

View File

@@ -77,7 +77,7 @@ func (c *Carver) ComputeSeams(p *Processor, img *image.NRGBA) (*image.NRGBA, err
dets := []pigo.Detection{} dets := []pigo.Detection{}
if p.PigoFaceDetector != nil && p.FaceDetect && detAttempts < maxFaceDetAttempts { if p.FaceDetector != nil && p.FaceDetect && detAttempts < maxFaceDetAttempts {
var ratio float64 var ratio float64
if width < height { 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. // 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. // 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. // 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 { if len(dets) == 0 {
// Retry detecting faces for a certain amount of time. // Retry detecting faces for a certain amount of time.

View File

@@ -250,7 +250,7 @@ func TestCarver_ShouldDetectFace(t *testing.T) {
} }
defer f.Close() defer f.Close()
p.PigoFaceDetector, err = p.PigoFaceDetector.Unpack(cascadeFile) p.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile)
if err != nil { if err != nil {
t.Fatalf("error unpacking the cascade file: %v", err) 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. // 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. // 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. // 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)) assert.Equal(1, len(faces))
} }
@@ -301,7 +301,7 @@ func TestCarver_ShouldNotRemoveFaceZone(t *testing.T) {
} }
defer f.Close() defer f.Close()
p.PigoFaceDetector, err = p.PigoFaceDetector.Unpack(cascadeFile) p.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile)
if err != nil { if err != nil {
t.Fatalf("error unpacking the cascade file: %v", err) 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. // 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. // 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. // 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. // 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. // 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() defer f.Close()
p.PigoFaceDetector, err = p.PigoFaceDetector.Unpack(cascadeFile) p.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile)
if err != nil { if err != nil {
t.Fatalf("error unpacking the cascade file: %v", err) 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. // 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. // 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. // 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 { for _, face := range faces {
if p.NewHeight < face.Scale { if p.NewHeight < face.Scale {

View File

@@ -1,23 +1,15 @@
package main package main
import ( import (
"errors"
"flag" "flag"
"fmt" "fmt"
"io"
"log" "log"
"os" "os"
"os/signal"
"path/filepath"
"runtime" "runtime"
"sync"
"syscall"
"time"
"gioui.org/app" "gioui.org/app"
"github.com/esimov/caire" "github.com/esimov/caire"
"github.com/esimov/caire/utils" "github.com/esimov/caire/utils"
"golang.org/x/term"
) )
const HelpBanner = ` const HelpBanner = `
@@ -33,22 +25,6 @@ Content aware image resize library.
// pipeName indicates that stdin/stdout is being used as file names. // pipeName indicates that stdin/stdout is being used as file names.
const pipeName = "-" 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. // Version indicates the current build version.
var Version string var Version string
@@ -71,13 +47,11 @@ var (
faceDetect = flag.Bool("face", false, "Use face detection") faceDetect = flag.Bool("face", false, "Use face detection")
faceAngle = flag.Float64("angle", 0.0, "Face rotation angle") faceAngle = flag.Float64("angle", 0.0, "Face rotation angle")
workers = flag.Int("conc", runtime.NumCPU(), "Number of files to process concurrently") workers = flag.Int("conc", runtime.NumCPU(), "Number of files to process concurrently")
// Common file related variable
fs os.FileInfo
) )
func main() { func main() {
log.SetFlags(0) log.SetFlags(0)
flag.Usage = func() { flag.Usage = func() {
fmt.Fprintf(os.Stderr, fmt.Sprintf(HelpBanner, Version)) fmt.Fprintf(os.Stderr, fmt.Sprintf(HelpBanner, Version))
flag.PrintDefaults() flag.PrintDefaults()
@@ -101,13 +75,6 @@ func main() {
SeamColor: *seamColor, 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) { if !(*newWidth > 0 || *newHeight > 0 || *percentage || *square) {
flag.Usage() flag.Usage()
log.Fatal(fmt.Sprintf("%s%s", log.Fatal(fmt.Sprintf("%s%s",
@@ -115,335 +82,21 @@ func main() {
utils.DefaultColor, utils.DefaultColor,
)) ))
} else { } else {
op := &caire.Ops{
Src: *source,
Dst: *destination,
Workers: *workers,
PipeName: pipeName,
}
if *preview { 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, // 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. // which have to run on the main OS thread of the operating systems like MacOS.
go execute(proc) go proc.Execute(op)
app.Main() app.Main()
} else { } else {
execute(proc) proc.Execute(op)
} }
} }
} }
// 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
}
}
return false
}

378
exec.go Normal file
View File

@@ -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
}

2
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/esimov/caire module github.com/esimov/caire
go 1.19 go 1.22
require ( require (
gioui.org v0.3.1 gioui.org v0.3.1

2
go.sum
View File

@@ -1,4 +1,5 @@
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY= 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 h1:hslYkrkIWvx28Mxe3A87opl+8s9mnWsnWmPDh11+zco=
gioui.org v0.3.1/go.mod h1:2atiYR4upH71/6ehnh6XsUELa7JZOrOHHNMDxGBZF0Q= gioui.org v0.3.1/go.mod h1:2atiYR4upH71/6ehnh6XsUELa7JZOrOHHNMDxGBZF0Q=
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= 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 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo=
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k= 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 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

View File

@@ -61,25 +61,25 @@ type enlargeFn func(*Carver, *image.NRGBA) (*image.NRGBA, error)
// Processor options // Processor options
type Processor struct { type Processor struct {
SobelThreshold int SobelThreshold int
BlurRadius int BlurRadius int
NewWidth int NewWidth int
NewHeight int NewHeight int
Percentage bool Percentage bool
Square bool Square bool
Debug bool Debug bool
Preview bool Preview bool
FaceDetect bool FaceDetect bool
ShapeType string ShapeType string
SeamColor string SeamColor string
MaskPath string MaskPath string
RMaskPath string RMaskPath string
Mask *image.NRGBA Mask *image.NRGBA
RMask *image.NRGBA RMask *image.NRGBA
GuiDebug *image.NRGBA GuiDebug *image.NRGBA
FaceAngle float64 FaceAngle float64
PigoFaceDetector *pigo.Pigo FaceDetector *pigo.Pigo
Spinner *utils.Spinner Spinner *utils.Spinner
vRes bool 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 { func (p *Processor) Process(r io.Reader, w io.Writer) error {
var err error var err error
// Instantiate a new Pigo object in case the face detection option is used.
p.PigoFaceDetector = pigo.NewPigo()
if p.FaceDetect { 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, // 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. // 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 { if err != nil {
return fmt.Errorf("error unpacking the cascade file: %v", err) 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) src, _, err := image.Decode(r)
if err != nil { if err != nil {
fmt.Println("err:", err)
os.Exit(2)
return err return err
} }

View File

@@ -2,10 +2,9 @@ package utils
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "log"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@@ -17,32 +16,35 @@ func DownloadImage(url string) (*os.File, error) {
// Retrieve the url and decode the response body. // Retrieve the url and decode the response body.
res, err := http.Get(url) res, err := http.Get(url)
if err != nil { 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() defer res.Body.Close()
data, err := ioutil.ReadAll(res.Body) data, err := io.ReadAll(res.Body)
if err != nil { 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 { 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. // Copy the image binary data into the temporary file.
_, err = io.Copy(tmpfile, bytes.NewBuffer(data)) _, err = io.Copy(tmpfile, bytes.NewBuffer(data))
if err != nil { 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()) ctype, err := DetectContentType(tmpfile.Name())
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !strings.Contains(ctype.(string), "image") { 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 return tmpfile, nil
} }
@@ -67,7 +69,11 @@ func DetectContentType(fname string) (interface{}, error) {
if err != nil { if err != nil {
return nil, err 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. // Only the first 512 bytes are used to sniff the content type.
buffer := make([]byte, 512) buffer := make([]byte, 512)