commit 0939116b54c2bdd9eec870cfa9404944181c8a7f Author: Mason Payne Date: Thu Jan 4 00:52:44 2024 -0700 create the program diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0cbc812 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +imgStack.exe +output.png \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/imgStack.iml b/.idea/imgStack.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/imgStack.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..adf0a73 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go new file mode 100644 index 0000000..a10b0b4 --- /dev/null +++ b/cmd/cli/cli.go @@ -0,0 +1,315 @@ +package main + +import ( + "fmt" + "image/color" + "image/png" + "path/filepath" + + "github.com/disintegration/imaging" + "github.com/urfave/cli/v2" + "golang.org/x/image/tiff" + "image" + "image/jpeg" + "io" + "mime" + "os" + "path" + "strings" +) + +func main() { + commands := []*cli.Command{ + // add commands here + } + + app := &cli.App{ + Name: "imgStack", + Usage: "A command line tool to denoise or give super resolution to an image by stacking multiple images together.", + Version: "v1.0.0", + Description: "imgStack is a command line tool to denoise or give super resolution to an image by stacking multiple images together.", + Commands: commands, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "output", + Value: "./output.png", + Usage: "path to output file (should be a png)", + Aliases: []string{ + "o", + }, + }, + &cli.BoolFlag{ + Name: "super-resolution", + Aliases: []string{"s", "sr"}, + Usage: "Whether to use super resolution or not. If this is set to true, the image will be doubled in size.", + Value: false, + }, + }, + Authors: []*cli.Author{ + { + Name: "Mason Payne", + Email: "mason@masonitestudios.com", + }, + }, + Copyright: "2024 Masonite Studios LLC", + UseShortOptionHandling: true, + Action: func(c *cli.Context) error { + var needsCleanup bool + var cleanUpFiles []string + var sourceFiles []string + // use the first argument as the file name + // we need to collect the list of discrete images + fmt.Println("Args: ", c.Args().Slice()) + + fileList, err := getListOfFiles(c.Args().Slice()) + if err != nil { + return fmt.Errorf("error getting list of files | %w", err) + } + + fmt.Println("Files to be used: ", fileList) + + // check if superResolution has been requested + superResolution := c.Bool("super-resolution") + + if len(fileList) == 0 { + return fmt.Errorf("no files provided") + } + + // loop over each file + for _, fileLocation := range fileList { + // make sure the image is either jpeg, png or tiff + fileMimeType := mime.TypeByExtension(path.Ext(fileLocation)) + if fileMimeType != "image/jpeg" && fileMimeType != "image/png" && fileMimeType != "image/tiff" { + return fmt.Errorf("unsupported file type | %v", fileMimeType) + } + + if superResolution { + // resize the images + tempLocation, err := resizeImage(fileLocation, 2) + 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 + } + sourceFiles = append(sourceFiles, fileLocation) // use the resized image + + fmt.Println("Needs cleanup: ", needsCleanup) + if needsCleanup { + cleanUpFiles = append(cleanUpFiles, tempLocation) + } + } else { + sourceFiles = append(sourceFiles, fileLocation) // use the original image + } + } + + err = stackImages(sourceFiles, c.String("output")) + if err != nil { + return fmt.Errorf("error stacking images | %w", err) + } + + if needsCleanup { + for _, file := range cleanUpFiles { + // remove the temp file + err = cleanUpTempFile(file) + if err != nil { + return fmt.Errorf("error removing temp file | %w", err) + } + } + } + + return nil + }, + } + + err := app.Run(os.Args) + if err != nil { + fmt.Println(fmt.Errorf("error running app | %w", err)) + return + } +} + +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, multiple int) (string, error) { + // load the image + var img image.Image + img, err := imaging.Open(fileLocation, imaging.AutoOrientation(true)) + if err != nil { + return "", fmt.Errorf("error opening image | %w", err) + } + + // double the size of the image + rect := img.Bounds() + width := rect.Max.X - rect.Min.X + height := rect.Max.Y - rect.Min.Y + x1 := multiple * width + y1 := multiple * height + + croppedImage := imaging.Fill(img, x1, y1, imaging.Center, imaging.Linear) // technically not cropped because it will still have the same aspect ratio + + // save the image to a temp file + tempFile, err := os.CreateTemp("", "imgStack*"+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 + fileMimeType := mime.TypeByExtension(path.Ext(fileLocation)) + if fileMimeType == "image/jpeg" { + err = jpeg.Encode(tempFile, croppedImage, nil) + if err != nil { + return "", fmt.Errorf("error encoding resized jpeg image | %w", err) + } + } + + if fileMimeType == "image/png" { + err = png.Encode(tempFile, croppedImage) + if err != nil { + return "", fmt.Errorf("error encoding resized png image | %w", err) + } + } + + if fileMimeType == "image/tiff" { + err = tiff.Encode(tempFile, croppedImage, nil) + if err != nil { + return "", fmt.Errorf("error encoding resized tiff image | %w", err) + } + } + + // return the temp file location + return tempFile.Name(), nil +} + +// stackImages stacks the images using the average value of each pixel in sourceFiles and saves the result to output +func stackImages(sourceFiles []string, output string) error { + // load the images + var images []image.Image + for _, fileLocation := range sourceFiles { + // load the image + var img image.Image + img, err := imaging.Open(fileLocation, imaging.AutoOrientation(true)) + if err != nil { + return fmt.Errorf("error opening image | %w", err) + } + images = append(images, img) + } + + // get the bounds of the first image + bounds := images[0].Bounds() + + // create a new image + newImage := image.NewRGBA(bounds) + + // loop over each pixel in the new image + for x := 0; x < bounds.Max.X; x++ { + for y := 0; y < bounds.Max.Y; y++ { + // get the average value of each pixel + var r, g, b, a uint32 + for _, img := range images { + // get the pixel at x, y + pixel := img.At(x, y) + // convert the pixel to RGBA + _r, _g, _b, _a := pixel.RGBA() + // get the values of the pixel + r += _r + g += _g + b += _b + a += _a + } + // get the average value of each pixel + + r = r / uint32(len(images)) + g = g / uint32(len(images)) + b = b / uint32(len(images)) + a = a / uint32(len(images)) + // set the pixel at x, y + + newImage.Set(x, y, &color.RGBA{ + R: uint8(r >> 8), + G: uint8(g >> 8), + B: uint8(b >> 8), + A: uint8(a >> 8), + }) + } + } + + // save the new image + err := saveImage(newImage, output) + if err != nil { + return fmt.Errorf("error saving image | %w", err) + } + + return nil +} + +func saveImage(img image.Image, output string) error { + // open the output file + outputFile, err := os.Create(output) + if err != nil { + return fmt.Errorf("error creating output file | %w", err) + } + defer outputFile.Close() + + // encode the image to the output file + fileMimeType := mime.TypeByExtension(path.Ext(output)) + if fileMimeType == "image/jpeg" { + err = jpeg.Encode(outputFile, img, nil) + if err != nil { + return fmt.Errorf("error encoding resized jpeg image | %w", err) + } + } + + if fileMimeType == "image/png" { + err = png.Encode(outputFile, img) + if err != nil { + return fmt.Errorf("error encoding resized png image | %w", err) + } + } + + if fileMimeType == "image/tiff" { + err = tiff.Encode(outputFile, img, nil) + if err != nil { + return fmt.Errorf("error encoding resized tiff image | %w", err) + } + } + + return nil +} + +func getListOfFiles(args []string) ([]string, error) { + var filenames []string + patterns := args + + if len(patterns) == 0 { + fmt.Println("No files provided, using stdin.") + // read from stdin + var err error + fileLocationBytes, err := io.ReadAll(os.Stdin) + if err != nil { + return nil, fmt.Errorf("error reading from stdin | %w", err) + } + patterns = strings.Split(string(fileLocationBytes), "\n") + } + + for _, pattern := range patterns { + subFileNames, err := filepath.Glob(pattern) + if err != nil { + return nil, fmt.Errorf("error getting list of files | %w", err) + } + filenames = append(filenames, subFileNames...) + } + + return filenames, nil +} diff --git a/cmd/cli/cli_test.go b/cmd/cli/cli_test.go new file mode 100644 index 0000000..edd5b1a --- /dev/null +++ b/cmd/cli/cli_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "reflect" + "testing" +) + +func Test_getListOfFiles(t *testing.T) { + type args struct { + args []string + } + tests := []struct { + name string + args args + want []string + wantErr bool + }{ + { + name: "test", + args: args{ + args: []string{"./cli*"}, + }, + want: []string{ + "cli.go", + "cli_test.go", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getListOfFiles(tt.args.args) + if (err != nil) != tt.wantErr { + t.Errorf("getListOfFiles() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getListOfFiles() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..150892c --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module imgStack + +go 1.21 + +require ( + github.com/disintegration/imaging v1.6.2 + github.com/urfave/cli/v2 v2.27.1 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e13ea7e --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +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/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +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.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..82f5a4e --- /dev/null +++ b/readme.md @@ -0,0 +1,3 @@ +# ImgStack + +Allows you to stack images in order to denoise or add super resolution to your images.