mirror of
https://github.com/esimov/caire.git
synced 2025-10-05 08:37:01 +08:00
Code refactoring and improvements
This commit is contained in:
@@ -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.
|
||||
|
@@ -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 {
|
||||
|
@@ -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)
|
||||
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
378
exec.go
Normal 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
2
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/esimov/caire
|
||||
|
||||
go 1.19
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
gioui.org v0.3.1
|
||||
|
2
go.sum
2
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=
|
||||
|
@@ -78,7 +78,7 @@ type Processor struct {
|
||||
RMask *image.NRGBA
|
||||
GuiDebug *image.NRGBA
|
||||
FaceAngle float64
|
||||
PigoFaceDetector *pigo.Pigo
|
||||
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
|
||||
}
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user