diff --git a/kadai2/sh-tatsuno/README.md b/kadai2/sh-tatsuno/README.md new file mode 100644 index 0000000..eb327f4 --- /dev/null +++ b/kadai2/sh-tatsuno/README.md @@ -0,0 +1,76 @@ +# 概要 +* 画像変換ツールgophotoの改良 +* io.Readerとio.Writerについて(下記) + +# 画像変換ツールgophoto +使い方としては下記のようにdirectoryを-d, -iで入力形式, -oで出力形式を指定する +-nで最大変換数を指定する. 現在の対応は.jpeg, .jpg, .png, .gif +```shell +$ gophoto -d dir -i .png -o .jpeg -n 10 +``` + + +# io.Readerとio.Writerの調査 +* 標準パッケージでどのように使われているか +* io.Readerとio.Writerがあることでどういう利点があるのか + +## 概要 +* io.Writer: 出力の抽象化 +* io.Reader: 入力の抽象化 + +```golang +type Writer interface { + Write(p []byte)(n int, err error) +} +``` + +```golang +type Reader interface { + Read(p []byte) (n int, err error) +} +``` + +ReadとWriteをそれぞれ実装したものが全てReader, Writerのインターフェースとして扱われる + +## 標準パッケージ +### Write/Readが実装されているもの +* syscall + * システムコールに利用する +* file + * ファイルの読み書き +* os.Stdout + * 標準出力 +* bytes.Buffer + * バイトへの読み書き +* strings.Builder + * Stringとのやりとり +* net.IPConn + * ネットワークへの読み書き + +## io.Writer +io.Writerという入出力に共通する処理を仕様として満たすインターフェース型を定義することでバイト列bを書き込んでバイト数n及びエラーeを出力する処理を通常のファイルに限定せずに汎用的に行うことができる + +例えば、 +io.MultiWriterのように複数のio.Writerを受け取り書き込み内容を同時に書き込む関数を使うことで、様々な形式の書き込みに対して抽象的に処理できる + +## io.Reader +読み込むデータ型がRead関数を実装していれば予め用意した[]byte形式のバッファに読み込むことができる。ただし、バッファ管理をしつつ、毎回格納するバッファのサイズを用意してそこに突っ込むのは面倒なため、ioutil.RealAllのような補助関数を利用することでより利便性高く利用できる + +## その他 +io.WriteString()のような関数がある。これらはbyteのsliceを使わずに直接stringに書き込むため標準のWriteメソッドではなくこちらを使った方が効率がいい + +## 類似するioインターフェース +* io.Closer + * Closeメソッドを持ち、使用したファイルを閉じる +* io.Seeker + * 読み書きの位置を移動する +* io.ReadAt + * 対象オブジェクトがランダムアクセスできるときに特定の位置に自由にアクセスできる + +## 参考 +Goならわかるシステムプログラミング, 渋川よしき, LambdaNote, 2017 +[How to use the io.Writer interface · YourBasic Go](https://yourbasic.org/golang/io-writer-interface-explained/) + +## テストカバレッジ +- conv: 71.4% +- dir: 91.7% \ No newline at end of file diff --git a/kadai2/sh-tatsuno/gophoto/conv/image_data.go b/kadai2/sh-tatsuno/gophoto/conv/image_data.go new file mode 100644 index 0000000..800ac10 --- /dev/null +++ b/kadai2/sh-tatsuno/gophoto/conv/image_data.go @@ -0,0 +1,91 @@ +package conv + +import ( + "image" + "image/gif" + "image/jpeg" + "image/png" + "os" + "path/filepath" + + "golang.org/x/xerrors" +) + +// ImageData image struct +type ImageData struct { + Path string + Data image.Image +} + +// NewImageData generate ImageData struct +func NewImageData(path string) (*ImageData, error) { + file, err := os.Open(path) + defer file.Close() + if err != nil { + return nil, err + } + image, _, err := image.Decode(file) + if err != nil { + return nil, err + } + return &ImageData{ + Path: path, + Data: image, + }, nil +} + +// Convert convert image +func (i *ImageData) Convert(ext string) error { + + // check path + if filepath.Ext(i.Path) == ext { + return nil + } + + // save file + newPath := i.Path[:len(i.Path)-len(filepath.Ext(i.Path))] + ext + + // if file exist, do nothing + if _, err := os.Stat(newPath); !os.IsNotExist(err) { + return nil + } + + i.Path = newPath + if err := func(ext string) error { + for _, suffix := range []string{".jpeg", ".jpg", ".gif", ".png"} { + if ext == suffix { + return nil + } + } + return xerrors.New("invalid extension") + }(ext); err != nil { + return err + } + + // if file exist, do nothing + if _, err := os.Stat(i.Path); !os.IsNotExist(err) { + return nil + } + + dst, err := os.Create(i.Path) + if err != nil { + return err + } + defer dst.Close() + + switch ext { + case ".jpeg", ".jpg": + err = jpeg.Encode(dst, i.Data, nil) + case ".gif": + err = gif.Encode(dst, i.Data, nil) + case ".png": + err = png.Encode(dst, i.Data) + default: + err = xerrors.New("error in main method") + } + if err != nil { + return err + } + return nil + +} diff --git a/kadai2/sh-tatsuno/gophoto/conv/image_data_test.go b/kadai2/sh-tatsuno/gophoto/conv/image_data_test.go new file mode 100644 index 0000000..60bd57c --- /dev/null +++ b/kadai2/sh-tatsuno/gophoto/conv/image_data_test.go @@ -0,0 +1,87 @@ +package conv + +import ( + "image/jpeg" + "image/png" + "os" + "path/filepath" + "testing" +) + +func Test_ImageData(t *testing.T) { + t.Helper() + t.Run("OK: .png -> .jpeg", func(t *testing.T) { + + // ### Given ### + img, err := NewImageData("./testdata/lena-png.png") + if err != nil { + t.Fatalf("Cannot load file. err: %v", err) + } + + // ### When ### + path := "./testdata/lena-png.jpeg" + if err = img.Convert(filepath.Ext(path)); err != nil { + t.Fatalf("Cannot save file. err: %v", err) + } + + // ### Then ### + file, err := os.Open(path) + if err != nil { + t.Fatalf("Cannot open saved file. err: %v", err) + } + if _, err = jpeg.Decode(file); err != nil { + t.Fatalf("Cannot decode saved file. err: %v", err) + } + if err := os.Remove(path); err != nil { + t.Fatalf("Cannot remove saved file. err: %v", err) + } + + }) + + t.Run("OK: .jpeg -> .png", func(t *testing.T) { + + // ### Given ### + img, err := NewImageData("./testdata/lena-jpeg.jpeg") + if err != nil { + t.Fatalf("Cannot load file. err: %v", err) + } + + // ### When ### + path := "./testdata/lena-jpeg.png" + if err = img.Convert(filepath.Ext(path)); err != nil { + t.Fatalf("Cannot save file. err: %v", err) + } + + // ### Then ### + file, err := os.Open(path) + if err != nil { + t.Fatalf("Cannot open saved file. err: %v", err) + } + if _, err = png.Decode(file); err != nil { + t.Fatalf("Cannot decode saved file. err: %v", err) + } + if err := os.Remove(path); err != nil { + t.Fatalf("Cannot remove saved file. err: %v", err) + } + + }) + + t.Run("NG: cannot openfile", func(t *testing.T) { + + // ### Given ### + _, err := NewImageData("./testdata/noexist.png") + if err == nil { + t.Fatal("Load no exist file.") + } + }) + + t.Run("NG: invalid extension", func(t *testing.T) { + + // ### Given ### + _, err := NewImageData("./img.go") + if err == nil { + t.Fatal("Load invalid file.") + } + }) + +} diff --git a/kadai2/sh-tatsuno/gophoto/conv/testdata/lena-jpeg.jpeg b/kadai2/sh-tatsuno/gophoto/conv/testdata/lena-jpeg.jpeg new file mode 100644 index 0000000..d89e554 Binary files /dev/null and b/kadai2/sh-tatsuno/gophoto/conv/testdata/lena-jpeg.jpeg differ diff --git a/kadai2/sh-tatsuno/gophoto/conv/testdata/lena-png.png b/kadai2/sh-tatsuno/gophoto/conv/testdata/lena-png.png new file mode 100644 index 0000000..ead4cd9 Binary files /dev/null and b/kadai2/sh-tatsuno/gophoto/conv/testdata/lena-png.png differ diff --git a/kadai2/sh-tatsuno/gophoto/dir/lookup.go b/kadai2/sh-tatsuno/gophoto/dir/lookup.go new file mode 100644 index 0000000..a06db10 --- /dev/null +++ b/kadai2/sh-tatsuno/gophoto/dir/lookup.go @@ -0,0 +1,36 @@ +package dir + +import ( + "io/ioutil" + "path/filepath" +) + +// Lookup lookup directory to find image files +func Lookup(dir string, ext string, pathList []string) ([]string, error) { + + fileInfos, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + + for _, fileInfo := range fileInfos { + + // get file info + path := filepath.Join(dir, fileInfo.Name()) + + // check if the path is about directory + if fileInfo.IsDir() { + pathList, err = Lookup(path, ext, pathList) + if err != nil { + return nil, err + } + } + + // check if the suffix is equal to the input format + if filepath.Ext(path) == ext { + pathList = append(pathList, path) + } + } + + return pathList, nil +} diff --git a/kadai2/sh-tatsuno/gophoto/dir/lookup_test.go b/kadai2/sh-tatsuno/gophoto/dir/lookup_test.go new file mode 100644 index 0000000..2e7fa7b --- /dev/null +++ b/kadai2/sh-tatsuno/gophoto/dir/lookup_test.go @@ -0,0 +1,52 @@ +package dir + +import ( + "reflect" + "testing" +) + +func Test_Lookup(t *testing.T) { + t.Helper() + + lookupOKTests := []struct { + testCase string + ext string + expected []string + }{ + {"OK: .jpg", ".jpg", []string{"testdata/test1.jpg", "testdata/test3/test4.jpg", "testdata/test3/test5.jpg"}}, + {"OK: .png", ".png", []string{"testdata/test2.png", "testdata/test3/test6.png"}}, + {"OK: .gif", ".gif", []string{}}, + } + for _, test := range lookupOKTests { + + t.Run(test.testCase, func(t *testing.T) { + actual := []string{} + // ### Given ### + actual, err := Lookup("./testdata", test.ext, actual) + if err != nil { + t.Fatalf("Failed test. err: %v", err) + } + + // ### Then ### + if !reflect.DeepEqual(actual, test.expected) { + t.Fatalf("Failed test. expected: %v,\n but actual: %v", test.expected, actual) + } + }) + } + + t.Run("NG: not found directory", func(t *testing.T) { + actual := []string{} + // ### Given ### + _, err := Lookup("./testdata2", ".jpg", actual) + if err == nil { + t.Fatalf("should be error.") + } + + expected := "open ./testdata2: no such file or directory" + + // ### Then ### + if err.Error() != expected { + t.Fatalf("Failed test. expected: %s,\n but actual: %s", expected, err.Error()) + } + }) +} diff --git a/kadai2/sh-tatsuno/gophoto/dir/testdata/test1.jpg b/kadai2/sh-tatsuno/gophoto/dir/testdata/test1.jpg new file mode 100644 index 0000000..e69de29 diff --git a/kadai2/sh-tatsuno/gophoto/dir/testdata/test2.png b/kadai2/sh-tatsuno/gophoto/dir/testdata/test2.png new file mode 100644 index 0000000..e69de29 diff --git a/kadai2/sh-tatsuno/gophoto/dir/testdata/test3/test4.jpg b/kadai2/sh-tatsuno/gophoto/dir/testdata/test3/test4.jpg new file mode 100644 index 0000000..e69de29 diff --git a/kadai2/sh-tatsuno/gophoto/dir/testdata/test3/test5.jpg b/kadai2/sh-tatsuno/gophoto/dir/testdata/test3/test5.jpg new file mode 100644 index 0000000..e69de29 diff --git a/kadai2/sh-tatsuno/gophoto/dir/testdata/test3/test6.png b/kadai2/sh-tatsuno/gophoto/dir/testdata/test3/test6.png new file mode 100644 index 0000000..e69de29 diff --git a/kadai2/sh-tatsuno/gophoto/main.go b/kadai2/sh-tatsuno/gophoto/main.go new file mode 100644 index 0000000..47b78ff --- /dev/null +++ b/kadai2/sh-tatsuno/gophoto/main.go @@ -0,0 +1,91 @@ +package main + +import ( + "flag" + "fmt" + "io" + "os" + + "github.com/gopherdojo/dojo5/kadai2/sh-tatsuno/gophoto/conv" + "github.com/gopherdojo/dojo5/kadai2/sh-tatsuno/gophoto/dir" +) + +const ( + // ExitCodeOK : exit code + ExitCodeOK = 0 + + // ExitCodeError : error code + ExitCodeError = 1 +) + +func usage() { + io.WriteString(os.Stderr, usageText) + flag.PrintDefaults() +} + +const usageText = `this is image convert library by go. + +In normal usage, you should set -d for directory and -i for input extension. +You also have to set output extension by -o. +You can also set maximum nuber you want to convert by set n. +current available extensions are jpg, jpeg, png, and gif. + +Example: + gophoto -d dir -i .png -o .jpeg -n 10 + +` + +func main() { + os.Exit(run(os.Args[1:])) +} + +func run(args []string) int { + var n int + var dirName, input, output string + + // args + flags := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) + flags.Usage = usage + flags.IntVar(&n, "n", 10, "number of maximum images to convert; default is 10.") + flags.StringVar(&dirName, "d", "", "directory path.") + flags.StringVar(&input, "i", "", "input extension.") + flags.StringVar(&output, "o", "", "output extension.") + flags.Parse(args) + + if dirName == "" { + usage() + fmt.Fprintf(os.Stderr, "Expected dir path\n") + return ExitCodeError + } + + // lookup + pathList := []string{} + pathList, err := dir.Lookup(dirName, input, pathList) + if err != nil { + fmt.Fprintf(os.Stderr, "can not open file, %v\n", err) + return ExitCodeError + } + + // convert + for i, path := range pathList { + if i > n { + break + } + + // convert in each file + img, err := conv.NewImageData(path) + if err != nil { + fmt.Fprintf(os.Stderr, "can not generate img instance, %v\n", err) + return ExitCodeError + } + + if err := img.Convert(output); err != nil { + fmt.Fprintf(os.Stderr, "can not convert file, %v\n", err) + return ExitCodeError + } + + } + + return ExitCodeOK + +}