diff --git a/kadai2/lfcd85/.gitignore b/kadai2/lfcd85/.gitignore new file mode 100644 index 0000000..cb29540 --- /dev/null +++ b/kadai2/lfcd85/.gitignore @@ -0,0 +1 @@ +bin/convert diff --git a/kadai2/lfcd85/Makefile b/kadai2/lfcd85/Makefile new file mode 100644 index 0000000..0ce4479 --- /dev/null +++ b/kadai2/lfcd85/Makefile @@ -0,0 +1,12 @@ +bin/convert: cmd/convert/*.go imgconv/*.go + GO111MODULE=on go build -o bin/convert cmd/convert/main.go + +fmt: + go fmt ./... + go vet ./... + +check: + GO111MODULE=on go test ./imgconv/... -v + +coverage: + GO111MODULE=on go test ./imgconv/... -cover diff --git a/kadai2/lfcd85/README.md b/kadai2/lfcd85/README.md new file mode 100644 index 0000000..fe9718c --- /dev/null +++ b/kadai2/lfcd85/README.md @@ -0,0 +1,29 @@ +# imgconv + +An implementation for the image conversion command, kadai-1 of Gopherdojo #5. + +Gopher道場 #5 課題1 `画像変換コマンド` の実装です。 + +## Installation + +```bash +$ make bin/convert +``` + +## Usage + +Specify the target directory as an argument. The given directory is recursively processed. Converted files are outputted under `./output/` directory. + +コマンド引数に対象ディレクトリを指定してください。ディレクトリ以下は再帰的に処理されます。変換後のファイルは `./output/` ディレクトリ以下に出力されます。 + +```bash +$ bin/convert test/ +``` + +Input and output image formats can be set by `-f` (from) and `-t` (to) options. Default formats are from JPEG to PNG. JPEG, PNG, GIF are available. + +画像形式は `-f` オプション(変換前)・ `-t` オプション(変換後)で指定できます。デフォルトは JPEG → PNG です。JPEG, PNG, GIF 形式が利用可能です。 + +```bash +$ bin/convert -f png -t jpeg test/ +``` diff --git a/kadai2/lfcd85/bin/.gitkeep b/kadai2/lfcd85/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/kadai2/lfcd85/cmd/convert/main.go b/kadai2/lfcd85/cmd/convert/main.go new file mode 100644 index 0000000..e96f994 --- /dev/null +++ b/kadai2/lfcd85/cmd/convert/main.go @@ -0,0 +1,22 @@ +// Command bin/convert ... +package main + +import ( + "flag" + "fmt" + + "github.com/gopherdojo/dojo5/kadai2/lfcd85/imgconv" +) + +func main() { + from := flag.String("f", "jpeg", "Image format before conversion (default: jpeg)") + to := flag.String("t", "png", "Image format after conversion (default: png)") + flag.Parse() + dirName := flag.Arg(0) + + err := imgconv.Convert(dirName, *from, *to) + if err != nil { + fmt.Println("error:", err) + return + } +} diff --git a/kadai2/lfcd85/go.mod b/kadai2/lfcd85/go.mod new file mode 100644 index 0000000..58fa07c --- /dev/null +++ b/kadai2/lfcd85/go.mod @@ -0,0 +1,3 @@ +module github.com/gopherdojo/dojo5/kadai2/lfcd85 + +go 1.12 diff --git a/kadai2/lfcd85/imgconv/imgconv.go b/kadai2/lfcd85/imgconv/imgconv.go new file mode 100644 index 0000000..8b2a6eb --- /dev/null +++ b/kadai2/lfcd85/imgconv/imgconv.go @@ -0,0 +1,182 @@ +// Package imgconv provides a recursive conversion of images in the directory. +package imgconv + +import ( + "errors" + "image" + "image/gif" + "image/jpeg" + "image/png" + "io" + "os" + "path/filepath" + "strings" +) + +// MapImgFmtExts is a map of the image formats and its extensions. +type MapImgFmtExts map[ImgFmt]Exts + +// Exts is a slice of image extensions. +type Exts []Ext + +// Ext is a image extension. +type Ext string + +// ImgFmt is a image format. +type ImgFmt string + +// Converter is a struct which contains info about image formats and extensions. +type Converter struct { + fmtFrom ImgFmt + fmtTo ImgFmt + imgFmtExts MapImgFmtExts +} + +// Convert recursively seeks a given directory and converts images from and to given formats. +func Convert(dir string, from string, to string) error { + if dir == "" { + return errors.New("directory name is not provided") + } + + cv := &Converter{} + cv.imgFmtExts.Init() + cv.fmtFrom.Detect(cv, from) + cv.fmtTo.Detect(cv, to) + if cv.fmtFrom == "" || cv.fmtTo == "" { + return errors.New("given image format is not supported") + } + if cv.fmtFrom == cv.fmtTo { + return errors.New("image formats before and after conversion are the same") + } + + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + err = cv.convSingleFile(path, info) + return err + }) + return err +} + +func (cv *Converter) convSingleFile(path string, info os.FileInfo) error { + if info.IsDir() { + outputPath := addOutputDir(path) + return os.MkdirAll(outputPath, 0777) + } + if !cv.fmtFrom.Match(cv, info.Name()) { + return nil + } + + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + img, err := cv.decodeImg(f) + if err != nil { + return nil + } + + return cv.writeOutputFile(img, path) +} + +func (cv *Converter) writeOutputFile(img image.Image, path string) error { + outputPath := cv.generateOutputPath(path) + + f, err := os.Create(outputPath) + if err != nil { + return err + } + defer f.Close() + + err = cv.encodeImg(f, img) + return err +} + +func (cv *Converter) decodeImg(r io.Reader) (image.Image, error) { + img, fmtStr, err := image.Decode(r) + if ImgFmt(fmtStr) != cv.fmtFrom { + err = errors.New("image format does not match") + } + return img, err +} + +func (cv *Converter) encodeImg(w io.Writer, img image.Image) error { + switch cv.fmtTo { + case "jpeg": + if err := jpeg.Encode(w, img, nil); err != nil { + return err + } + case "png": + if err := png.Encode(w, img); err != nil { + return err + } + case "gif": + if err := gif.Encode(w, img, nil); err != nil { + return err + } + } + return nil +} + +func (cv *Converter) generateOutputPath(path string) string { + dirAndBase := strings.TrimRight(path, filepath.Ext(path)) + ext := cv.imgFmtExts.ConvToExt(cv.fmtTo) + path = strings.Join([]string{dirAndBase, string(ext)}, ".") + return addOutputDir(path) +} + +func addOutputDir(path string) string { + return strings.Join([]string{ + "./output", + strings.TrimLeft(path, "./"), + }, "/") +} + +// Detect specifies image format from file extension string. +func (imgFmt *ImgFmt) Detect(cv *Converter, extStr string) { + ext := Ext(strings.ToLower(extStr)) + *imgFmt = cv.imgFmtExts.ConvToImgFmt(ext) +} + +// Match checks whether the file has an extension of the image format. +func (imgFmt ImgFmt) Match(cv *Converter, fileName string) bool { + fileExtStr := strings.TrimPrefix(filepath.Ext(fileName), ".") + fileExt := Ext(strings.ToLower(fileExtStr)) + fileImgFmt := cv.imgFmtExts.ConvToImgFmt(fileExt) + return fileImgFmt == imgFmt +} + +// Init creates the map of image formats and its extensions available. +func (m *MapImgFmtExts) Init() { + *m = MapImgFmtExts{ + "jpeg": Exts{"jpg", "jpeg"}, + "png": Exts{"png"}, + "gif": Exts{"gif"}, + } +} + +// ConvToImgFmt converts image extension to its format. +func (m MapImgFmtExts) ConvToImgFmt(ext Ext) ImgFmt { + for imgFmt, fmtExts := range m { + for _, fmtExt := range fmtExts { + if ext == fmtExt { + return imgFmt + } + } + } + return "" +} + +// ConvToExt converts image format to its extension. +func (m MapImgFmtExts) ConvToExt(imgFmt ImgFmt) Ext { + for keyImgFmt, fmtExts := range m { + if imgFmt == keyImgFmt { + return fmtExts[0] + } + } + return "" +} diff --git a/kadai2/lfcd85/imgconv/imgconv_test.go b/kadai2/lfcd85/imgconv/imgconv_test.go new file mode 100644 index 0000000..a1927f4 --- /dev/null +++ b/kadai2/lfcd85/imgconv/imgconv_test.go @@ -0,0 +1,179 @@ +package imgconv + +import ( + "os" + "strings" + "testing" +) + +func assertEq(t *testing.T, actual interface{}, expected interface{}) { + t.Helper() + if actual != expected { + t.Errorf("actual: %v, expected: %v", actual, expected) + } +} + +func assertFileExists(t *testing.T, filePath string, expected bool) { + t.Helper() + + _, err := os.Stat(filePath) + actual := err == nil + + if actual != expected { + switch expected { + case true: + t.Errorf("file %v should exist but does not", filePath) + case false: + t.Errorf("file %v should not exist but does", filePath) + } + } +} + +func TestConvert(t *testing.T) { + cases := []struct { + from string + to string + expectedSuccess bool + expectedFileNames map[string]bool + }{ + {"jpeg", "png", true, map[string]bool{ + "not_image.png": false, + "not_image2.png": false, + "sample1.png": true, + "sample2.png": true, + "sample4.png": false, + "child_dir/sample3.png": true, + "child_dir/sample4.png": false, + "child_dir/sample5.png": false, + }}, + {"png", "gif", true, map[string]bool{ + "not_image.gif": false, + "not_image2.gif": false, + "sample1.gif": false, + "sample2.gif": false, + "sample4.gif": true, + "child_dir/sample3.gif": false, + "child_dir/sample4.gif": true, + "child_dir/sample5.gif": false, + }}, + {"gif", "jpeg", true, map[string]bool{ + "not_image.jpg": false, + "not_image2.jpg": false, + "sample1.jpg": false, + "sample2.jpg": false, + "sample4.jpg": false, + "child_dir/sample3.jpg": false, + "child_dir/sample4.jpg": false, + "child_dir/sample5.jpg": true, + }}, + {"jpeg", "jpeg", false, nil}, + {"rb", "go", false, nil}, + } + + outputDir := "./output/testdata" + for _, c := range cases { + c := c + t.Run(strings.Join([]string{c.from, c.to}, "->"), func(t *testing.T) { + defer os.RemoveAll("./output") + + err := Convert("../testdata", c.from, c.to) + if err != nil && c.expectedSuccess == true { + t.Errorf("function Convert is expected to succeed, but actually failed") + } + if err == nil && c.expectedSuccess == false { + t.Errorf("function Convert is expected to fail, but actually succeeded") + } + for f, b := range c.expectedFileNames { + filePath := strings.Join([]string{outputDir, f}, "/") + assertFileExists(t, filePath, b) + } + }) + } +} + +func TestConverter_GenerateOutputPath(t *testing.T) { + cases := []struct { + path string + fmtTo ImgFmt + expected string + }{ + { + "path/to/hoge.jpg", + ImgFmt("png"), + "./output/path/to/hoge.png", + }, + { + "./path/to/fuga.PNG", + ImgFmt("jpeg"), + "./output/path/to/fuga.jpg", + }, + { + "piyo.png", + ImgFmt("gif"), + "./output/piyo.gif", + }, + { + "../../path/to/foobar.gif", + ImgFmt("jpeg"), + "./output/path/to/foobar.jpg", + }, + } + + cv := &Converter{} + cv.imgFmtExts.Init() + for _, c := range cases { + c := c + t.Run(c.path, func(t *testing.T) { + cv.fmtTo = c.fmtTo + assertEq(t, cv.generateOutputPath(c.path), c.expected) + }) + } +} + +func TestImgFmt_Detect(t *testing.T) { + cases := []struct { + extStr string + expected ImgFmt + }{ + {"png", ImgFmt("png")}, + {"jpg", ImgFmt("jpeg")}, + {"JPEG", ImgFmt("jpeg")}, + {"GIF", ImgFmt("gif")}, + } + + cv := &Converter{} + cv.imgFmtExts.Init() + for _, c := range cases { + c := c + t.Run(c.extStr, func(t *testing.T) { + var imgFmt ImgFmt + imgFmt.Detect(cv, c.extStr) + assertEq(t, imgFmt, c.expected) + }) + } +} + +func TestImgFmt_Match(t *testing.T) { + cases := []struct { + fileName string + imgFmt ImgFmt + expected bool + }{ + {"hoge.jpg", ImgFmt("jpeg"), true}, + {"fuga.png", ImgFmt("gif"), false}, + {"piyo.png", ImgFmt("png"), true}, + {"foo.js", ImgFmt("png"), false}, + {".JPEG", ImgFmt("jpeg"), true}, + {"jpeg", ImgFmt("jpeg"), false}, + {"foopng", ImgFmt("png"), false}, + } + + cv := &Converter{} + cv.imgFmtExts.Init() + for _, c := range cases { + c := c + t.Run(c.fileName, func(t *testing.T) { + assertEq(t, c.imgFmt.Match(cv, c.fileName), c.expected) + }) + } +} diff --git a/kadai2/lfcd85/testdata/child_dir/not_image.txt b/kadai2/lfcd85/testdata/child_dir/not_image.txt new file mode 100644 index 0000000..b8d665d --- /dev/null +++ b/kadai2/lfcd85/testdata/child_dir/not_image.txt @@ -0,0 +1 @@ +This is a text file, not an image. diff --git a/kadai2/lfcd85/testdata/child_dir/not_image2.jpg b/kadai2/lfcd85/testdata/child_dir/not_image2.jpg new file mode 100644 index 0000000..ad89fab --- /dev/null +++ b/kadai2/lfcd85/testdata/child_dir/not_image2.jpg @@ -0,0 +1 @@ +This file has a extension of jpg, but actually is a text file. diff --git a/kadai2/lfcd85/testdata/child_dir/sample3.jpg b/kadai2/lfcd85/testdata/child_dir/sample3.jpg new file mode 100644 index 0000000..2bf5645 Binary files /dev/null and b/kadai2/lfcd85/testdata/child_dir/sample3.jpg differ diff --git a/kadai2/lfcd85/testdata/child_dir/sample4.png b/kadai2/lfcd85/testdata/child_dir/sample4.png new file mode 100644 index 0000000..a5d38d7 Binary files /dev/null and b/kadai2/lfcd85/testdata/child_dir/sample4.png differ diff --git a/kadai2/lfcd85/testdata/child_dir/sample5.gif b/kadai2/lfcd85/testdata/child_dir/sample5.gif new file mode 100644 index 0000000..6275211 Binary files /dev/null and b/kadai2/lfcd85/testdata/child_dir/sample5.gif differ diff --git a/kadai2/lfcd85/testdata/sample1.jpg b/kadai2/lfcd85/testdata/sample1.jpg new file mode 100644 index 0000000..dd33dfe Binary files /dev/null and b/kadai2/lfcd85/testdata/sample1.jpg differ diff --git a/kadai2/lfcd85/testdata/sample2.jpg b/kadai2/lfcd85/testdata/sample2.jpg new file mode 100644 index 0000000..f143791 Binary files /dev/null and b/kadai2/lfcd85/testdata/sample2.jpg differ diff --git a/kadai2/lfcd85/testdata/sample4.png b/kadai2/lfcd85/testdata/sample4.png new file mode 100644 index 0000000..62a6b15 Binary files /dev/null and b/kadai2/lfcd85/testdata/sample4.png differ