diff --git a/.gitignore b/.gitignore index 6c7e23a..7033af3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ run_i2v.exe run_i2v_gjr.exe output.mp4 resp.txt -test.png \ No newline at end of file +test.png +resized.jpg diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index 1796900..8353694 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -5,7 +5,11 @@ import ( b64 "encoding/base64" "encoding/json" "fmt" - "github.com/urfave/cli/v2" + "image/draw" + + "image" + "image/jpeg" + "image/png" "io" "mime" "mime/multipart" @@ -15,12 +19,15 @@ import ( "path" "strings" "time" + + "github.com/nfnt/resize" + "github.com/urfave/cli/v2" ) func main() { commands := []*cli.Command{ - //startCmd(), getJobResultCmd(), + resizeCmd(), } app := &cli.App{ @@ -38,6 +45,12 @@ func main() { "o", }, }, + &cli.StringFlag{ + Name: "format", + Aliases: []string{"f"}, + Usage: "The format to resize the image to wide, tall, or square", + Value: "wide", + }, }, Authors: []*cli.Author{ { @@ -48,27 +61,25 @@ func main() { Copyright: "2023 Masonite Studios LLC", UseShortOptionHandling: true, Action: func(c *cli.Context) error { + var needsCleanup bool // use the first argument as the file name - fileLocation := c.Args().Get(0) - // if no argument is provided, use stdin - if fileLocation == "" { - fmt.Println("No file provided, using stdin.") - // read from stdin - var err error - fileLocationBytes, err := io.ReadAll(os.Stdin) - if err != nil { - return fmt.Errorf("error reading from stdin | %w", err) - } - fileLocation = string(fileLocationBytes) + fileLocation, err := getFileLocation(c) + if err != nil { + return fmt.Errorf("error getting file location | %w", err) } - // TODO: preprocess the image to make sure it is the right size + // preprocess the image to make sure it is the right size /* Supported Dimensions: 1024x576 576x1024 768x768 */ + // get the format + format := c.String("format") + if format != "wide" && format != "tall" && format != "square" { + return fmt.Errorf("invalid format %s", format) + } // make sure the image is either jpeg or png fileMimeType := mime.TypeByExtension(path.Ext(fileLocation)) @@ -76,8 +87,27 @@ func main() { return fmt.Errorf("unsupported file type | %v", fileMimeType) } + // resize the image + tempLocation, err := resizeImage(fileLocation, format) + if err != nil { + return fmt.Errorf("error resizing image | %w", err) + } + + if tempLocation != fileLocation { + fmt.Printf("Resized image to %s\n", tempLocation) + needsCleanup = true + fileLocation = tempLocation // use the resized image + } + id, err := initiateGeneratingAnimation(fileLocation) if err != nil { + if needsCleanup { + // remove the temp file + err = cleanUpTempFile(tempLocation) + if err != nil { + return fmt.Errorf("error removing temp file | %w", err) + } + } return fmt.Errorf("error making request | %w", err) } @@ -87,6 +117,13 @@ func main() { // wait for the job to finish err = job(id, c.String("output")) if err != nil { + if needsCleanup { + // remove the temp file + err = cleanUpTempFile(tempLocation) + if err != nil { + return fmt.Errorf("error removing temp file | %w", err) + } + } return fmt.Errorf("error getting job result | %w", err) } @@ -101,6 +138,152 @@ func main() { } } +func cleanUpTempFile(fileLocation string) error { + // remove the temp file + err := os.Remove(fileLocation) + if err != nil { + return fmt.Errorf("error removing temp file | %w", err) + } + return nil +} + +func resizeImage(fileLocation string, format string) (string, error) { + // load the image + file, err := os.Open(fileLocation) + if err != nil { + return "", fmt.Errorf("error opening file | %w", err) + } + defer file.Close() + + var img image.Image + fileMimeType := mime.TypeByExtension(path.Ext(fileLocation)) + if fileMimeType == "image/jpeg" { + // decode the image + img, err = jpeg.Decode(file) + if err != nil { + return "", fmt.Errorf("error decoding jpeg image | %w", err) + } + } else if fileMimeType == "image/png" { + // decode the image + img, err = png.Decode(file) + if err != nil { + return "", fmt.Errorf("error decoding png image | %w", err) + } + } else { + return "", fmt.Errorf("unsupported file type | %v", fileMimeType) + } + + // check if the image is already the correct size + rect := img.Bounds() + width := rect.Max.X - rect.Min.X + height := rect.Max.Y - rect.Min.Y + x1 := 1024 + y1 := 576 + if format == "wide" { + if width == 1024 && height == 576 { + fmt.Println("Image is already the correct size.") + return fileLocation, nil + } + } + if format == "tall" { + if width == 576 && height == 1024 { + fmt.Println("Image is already the correct size.") + return fileLocation, nil + } + x1 = 576 + y1 = 1024 + } + if format == "square" { + if width == 768 && height == 768 { + fmt.Println("Image is already the correct size.") + return fileLocation, nil + } + x1 = 768 + y1 = 768 + } + + inFormat := "wide" + if width < height { + inFormat = "tall" + } + if width == height { + inFormat = "square" + } + + // if not, resize the image + // scale the original image to the new size + var resizedImage image.Image + if format == "wide" { + resizedImage = resize.Resize(uint(x1), 0, img, resize.Lanczos3) + } + if format == "tall" { + resizedImage = resize.Resize(0, uint(y1), img, resize.Lanczos3) + } + if format == "square" { + if inFormat == "wide" { + resizedImage = resize.Resize(0, uint(y1), img, resize.Lanczos3) + } + if inFormat == "tall" { + resizedImage = resize.Resize(uint(x1), 0, img, resize.Lanczos3) + } + if inFormat == "square" { + resizedImage = resize.Resize(uint(x1), uint(y1), img, resize.Lanczos3) + } + } + + // crop the image to the final correct size + + // start by getting the center of the image + tempBounds := resizedImage.Bounds() + x0 := tempBounds.Max.X/2 - x1/2 + y0 := tempBounds.Max.Y/2 - y1/2 + xMax := x0 + x1 + yMax := y0 + y1 + + croppedImageRect := image.Rect(x0, y0, xMax, yMax) + rgba := convertToRGBA(resizedImage) + croppedImage := rgba.SubImage(croppedImageRect) + + // save the image to a temp file + tempFile, err := os.CreateTemp("", "i2v*"+path.Ext(fileLocation)) + if err != nil { + return "", fmt.Errorf("error creating temp file | %w", err) + } + defer tempFile.Close() + + // encode the image to the temp file + err = jpeg.Encode(tempFile, croppedImage, nil) + if err != nil { + return "", fmt.Errorf("error encoding resized image | %w", err) + } + + // return the temp file location + return tempFile.Name(), nil +} + +func convertToRGBA(img image.Image) *image.RGBA { + bounds := img.Bounds() + rgba := image.NewRGBA(bounds) + draw.Draw(rgba, bounds, img, bounds.Min, draw.Src) + return rgba +} + +func getFileLocation(c *cli.Context) (string, error) { + fileLocation := c.Args().Get(0) + // if no argument is provided, use stdin + if fileLocation == "" { + fmt.Println("No file provided, using stdin.") + // read from stdin + var err error + fileLocationBytes, err := io.ReadAll(os.Stdin) + if err != nil { + return "", fmt.Errorf("error reading from stdin | %w", err) + } + fileLocation = string(fileLocationBytes) + } + return fileLocation, nil +} + func initiateGeneratingAnimation(fileLocation string) (string, error) { // get base filename from file location filename := path.Base(fileLocation) diff --git a/cmd/cli/commands.go b/cmd/cli/commands.go index 4c5cc76..90c4ead 100644 --- a/cmd/cli/commands.go +++ b/cmd/cli/commands.go @@ -3,19 +3,129 @@ package main import ( "fmt" "github.com/urfave/cli/v2" + "io" + "os" ) -//func startCmd() *cli.Command { -// return &cli.Command{ -// Name: "start", -// Aliases: []string{"s"}, -// Usage: "Start the application service", -// Action: func(c *cli.Context) error { -// -// return nil -// }, -// } -//} +func resizeCmd() *cli.Command { + return &cli.Command{ + Name: "resize", + Aliases: []string{"r"}, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "output", + Value: "./resized.jpg", + Usage: "path to output resized file (should be a jpg)", + Aliases: []string{ + "o", + }, + }, + &cli.StringFlag{ + Name: "format", + Aliases: []string{"f"}, + Usage: "The format to resize the image to wide, tall, or square", + Value: "wide", + }, + }, + Usage: "Resize an image to a supported size for the API", + Action: func(c *cli.Context) error { + var needsCleanup bool + // get the image + // use the first argument as the file name + fileLocation, err := getFileLocation(c) + if err != nil { + return fmt.Errorf("error getting file location | %w", err) + } + + // get the format + format := c.String("format") + if format != "wide" && format != "tall" && format != "square" { + return fmt.Errorf("invalid format %s", format) + } + + // get output file + output := c.String("output") + + // resize the image + tempLocation, err := resizeImage(fileLocation, format) + if err != nil { + return fmt.Errorf("error resizing image | %w", err) + } + + if tempLocation != fileLocation { + fmt.Printf("Resized image to %s\n", tempLocation) + needsCleanup = true + } + + // copy the temp image to the output file + err = copyFile(tempLocation, output, 1024) + if err != nil { + return fmt.Errorf("error copying file | %w", err) + } + + if needsCleanup { + // remove the temp file + err = cleanUpTempFile(tempLocation) + if err != nil { + return fmt.Errorf("error removing temp file | %w", err) + } + } + + return nil + }, + } +} + +// copyFile copies a file from src to dst +// BUFFERSIZE is the size of the buffer to use when copying +// https://opensource.com/article/18/6/copying-files-go +func copyFile(src, dst string, BUFFERSIZE int64) error { + sourceFileStat, err := os.Stat(src) + if err != nil { + return err + } + + if !sourceFileStat.Mode().IsRegular() { + return fmt.Errorf("%s is not a regular file.", src) + } + + source, err := os.Open(src) + if err != nil { + return err + } + defer source.Close() + + _, err = os.Stat(dst) + if err == nil { + return fmt.Errorf("File %s already exists.", dst) + } + + destination, err := os.Create(dst) + if err != nil { + return err + } + defer destination.Close() + + if err != nil { + panic(err) + } + + buf := make([]byte, BUFFERSIZE) + for { + n, err := source.Read(buf) + if err != nil && err != io.EOF { + return err + } + if n == 0 { + break + } + + if _, err := destination.Write(buf[:n]); err != nil { + return err + } + } + return err +} func getJobResultCmd() *cli.Command { return &cli.Command{ diff --git a/go.mod b/go.mod index c4ed2ae..6288509 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module i2v go 1.21 -require github.com/urfave/cli/v2 v2.26.0 +require ( + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 + github.com/urfave/cli/v2 v2.26.0 +) require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect diff --git a/go.sum b/go.sum index 1d06da2..ab4729f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=