diff --git a/kadai1/segakazzz/.gitignore b/kadai1/segakazzz/.gitignore new file mode 100644 index 0000000..5de97bb --- /dev/null +++ b/kadai1/segakazzz/.gitignore @@ -0,0 +1,7 @@ +.idea +testdata/in/* +testdata/out/* +testdata/*.jpg +testdata/*.png +kadai1-segakazzz + diff --git a/kadai1/segakazzz/README.md b/kadai1/segakazzz/README.md new file mode 100644 index 0000000..74dfb11 --- /dev/null +++ b/kadai1/segakazzz/README.md @@ -0,0 +1,52 @@ +# 課題 1 画像変換コマンドを作ろう + +## 課題内容 + +### 次の仕様を満たすコマンドを作って下さい + +- ディレクトリを指定する +- 指定したディレクトリ以下の JPG ファイルを PNG に変換(デフォルト) +- ディレクトリ以下は再帰的に処理する +- 変換前と変換後の画像形式を指定できる(オプション) + +### 以下を満たすように開発してください + +- main パッケージと分離する +- 自作パッケージと標準パッケージと準標準パッケージのみ使う +- 準標準パッケージ:golang.org/x 以下のパッケージ +- ユーザ定義型を作ってみる +- GoDoc を生成してみる +- Go Modules を使ってみる + +## 回答・動作例 + +- 自作パッケージとして  github.com/gopherdojo/dojo8/kadai1/segakazzz/imgconv を作成 +- imgconv.RunConverter()でメインの処理を実行 +- -d オプションで指定したディレクトリの画像をソースとして使用する +- -d オプションで指定したディレクトリ内に out フォルダが作成され、出力される +- -i オプションで入力画像の  拡張子を指定可能(jpg or png) デフォルトは jpg +- -o オプションで出力画像の拡張子を指定可能(png or jpg) デフォルトは png +- (補足) image フォルダ内の download.zsh 実行して、[The Cat API](https://thecatapi.com/) から jpg ファイルを 10 個ダウンロード可能 + +### 動作例 main.go + +``` +$ go build -o kadai1-segakazzz +$ ./kadai1-segakazzz -d [imagedir] -i [jpg|png] -o [png|jpg] +``` + +``` +package main + +import "github.com/gopherdojo/dojo8/kadai1/segakazzz/imgconv" + +func main() { + imgconv.RunConverter() +} + +``` + +### 感想等 + +- Go Modules については、使用するパッケージのバージョン管理に使用されるものと理解しましたが、今回使用したパッケージは標準パッケージのみで、バージョンを指定する必要はなさそうでしたので require の部分は書いていません。 +- パッケージの書き方の標準を理解しておらず、勘に頼っているところがあるので、良い書き方を知りたいです。 diff --git a/kadai1/segakazzz/go.mod b/kadai1/segakazzz/go.mod new file mode 100644 index 0000000..e847375 --- /dev/null +++ b/kadai1/segakazzz/go.mod @@ -0,0 +1,3 @@ +module github.com/gopherdojo/dojo8/kadai1/segakazzz + +go 1.14 diff --git a/kadai1/segakazzz/go.sum b/kadai1/segakazzz/go.sum new file mode 100644 index 0000000..a632da4 --- /dev/null +++ b/kadai1/segakazzz/go.sum @@ -0,0 +1 @@ +github.com/gopherdojo/dojo8 v0.0.0-20200703052727-6a79d18126bf h1:lpYevjFQMxI5VNBc3WXV6Z5pDDrdppdDKwmeBoyt5BE= diff --git a/kadai1/segakazzz/imgconv/imgconv.go b/kadai1/segakazzz/imgconv/imgconv.go new file mode 100644 index 0000000..cd19e19 --- /dev/null +++ b/kadai1/segakazzz/imgconv/imgconv.go @@ -0,0 +1,176 @@ +// Package imgconv is for Gopher Dojo Kadai1 +package imgconv + +import ( + "flag" + "fmt" + "image" + "image/jpeg" + "image/png" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" +) + +type converter struct { + dirname string + input string + output string +} + +// RunConverter converts all image files in the directory which you indicate with -d option. +// If the process is completed succeessfully, you will see the list of output files and "Done!" +// message in the standard output. +func RunConverter() error { + var ( + dir = flag.String("d", ".", "Indicate directory to convert") + in = flag.String("i", "jpg", "Indicate input image file's extension") + out = flag.String("o", "png", "Indicate output image file's extension") + ) + + flag.Parse() + c, err := newConverter(*dir, *in, *out) + if err != nil { + // log.Fatal(err) + return err + } + err = c.Convert() + if err != nil { + // log.Fatal(err) + return err + } + fmt.Println("Done!") + return nil +} + +func newConverter(dirname string, input string, output string) (*converter, error) { + switch input { + case "jpg", "png": + input = strings.ToLower(input) + default: + return nil, fmt.Errorf("Input extension is not valid. Select one from jpg/png") + } + switch output { + case "jpg", "png": + output = strings.ToLower(output) + default: + return nil, fmt.Errorf("Output extension is not valid. Select one from jpg/png") + } + + if input == output { + return nil, fmt.Errorf("Input and Output extensiton is the same. No convertion is needed") + } + return &converter{dirname: dirname, input: input, output: output}, nil +} + +// Convert method converts all jpg files in dirname to png. "out" folder is generated if it doesn't exist. +func (c *converter) Convert() error { + files, e := c.getSourceFiles() + if e != nil { + return e + } + e = c.convertFiles(files) + if e != nil { + return e + } + return nil +} + +func (c *converter) getSourceFiles() ([]os.FileInfo, error) { + files, err := ioutil.ReadDir(c.dirname) + if err != nil { + return nil, err + } + return files, nil +} + +func (c *converter) convertFiles(files []os.FileInfo) error { + re, e := regexp.Compile("." + c.input + "$") + if e != nil { + return e + } + for _, file := range files { + if re.MatchString(file.Name()) { + e = c.convertSingle(file.Name()) + if e != nil { + return e + } + } + } + return nil +} + +func (c *converter) convertSingle(filename string) (e error) { + input := filepath.Join(c.dirname, filename) + outDir := filepath.Join(c.dirname, "out") + output := filepath.Join(outDir, strings.Replace(strings.ToLower(filename), "."+c.input, "."+c.output, -1)) + fmt.Println(output) + if !c.dirExists(outDir) { + os.Mkdir(outDir, 0755) + } + + in, e := os.Open(input) + if e != nil { + return e + } + + defer func() { + e = in.Close() + }() + + var out *os.File + if c.fileExists(output) { + out, e = os.OpenFile(output, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + } else { + out, e = os.Create(output) + } + if e != nil { + return e + } + + defer func() { + e = out.Close() + }() + + var ( + img image.Image + ) + switch c.input { + case "jpg": + img, e = jpeg.Decode(in) + case "png": + img, e = png.Decode(in) + } + + if e != nil { + return e + } + switch c.output { + case "png": + e = png.Encode(out, img) + case "jpg": + e = jpeg.Encode(out, img, nil) + } + if e != nil { + return e + } + return nil +} + +func (c *converter) fileExists(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} + +func (c *converter) dirExists(dirname string) bool { + info, err := os.Stat(dirname) + if os.IsNotExist(err) { + return false + } + return info.IsDir() +} diff --git a/kadai1/segakazzz/main.go b/kadai1/segakazzz/main.go new file mode 100644 index 0000000..081416d --- /dev/null +++ b/kadai1/segakazzz/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "os" + + "github.com/gopherdojo/dojo8/kadai1/segakazzz/imgconv" +) + +func main() { + err := imgconv.RunConverter() + if err != nil { + os.Exit(1) + } + os.Exit(0) +} diff --git a/kadai1/segakazzz/testdata/download.zsh b/kadai1/segakazzz/testdata/download.zsh new file mode 100755 index 0000000..e9fd9f0 --- /dev/null +++ b/kadai1/segakazzz/testdata/download.zsh @@ -0,0 +1,12 @@ +#!/bin/zsh + +curl https://cdn2.thecatapi.com/images/8er.jpg > 001.jpg +curl https://cdn2.thecatapi.com/images/ceh.jpg > 002.jpg +curl https://cdn2.thecatapi.com/images/MTU2MjQ4NA.jpg > 003.jpg +curl https://cdn2.thecatapi.com/images/5qc.jpg > 004.jpg +curl https://cdn2.thecatapi.com/images/962.jpg > 005.jpg +curl https://cdn2.thecatapi.com/images/MTc2Mzc0Mw.jpg > 006.jpg +curl https://cdn2.thecatapi.com/images/bos.jpg > 007.jpg +curl https://cdn2.thecatapi.com/images/SX2DvLw7u.jpg > 008.jpg +curl https://cdn2.thecatapi.com/images/9eh.jpg > 009.jpg +curl https://cdn2.thecatapi.com/images/7bo.jpg > 010.jpg \ No newline at end of file diff --git a/kadai2/segakazzz/.gitignore b/kadai2/segakazzz/.gitignore new file mode 100644 index 0000000..2a79263 --- /dev/null +++ b/kadai2/segakazzz/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +testdata/*.jpg +testdata/*.png +testdata/out +testdata/test +testdata/error \ No newline at end of file diff --git a/kadai2/segakazzz/README.md b/kadai2/segakazzz/README.md new file mode 100644 index 0000000..05e1fa7 --- /dev/null +++ b/kadai2/segakazzz/README.md @@ -0,0 +1,216 @@ +# Try 1: io.Reader と io.Writer について調べてみよう + +## 標準パッケージでどのように使われているか(回答) + +### io.Reader/io.Writer とは + +- golang.org の定義によると、それぞれメソッド Read/Write を持つインターフェイス +- io.Reader -> バイト列を読み、Read 関数のバイトスライスへ格納 +- io.Writer -> Write 関数の引数バイトスライスを書き出し +- インターフェースで宣言されているメソッドがすべて実装されている構造体ならばどんな型でも対象のインターフェイスとして扱うことができる + +参照)https://golang.org/pkg/io/ + +``` +type Reader interface { + Read(p []byte) (n int, err error) +} +``` + +``` +type Writer interface { + Write(p []byte) (n int, err error) +} +``` + +### 標準パッケージでの使用 + +リンクの通り多くのパッケージで使用されている + +- io.Reader https://golang.org/search?q=Read#Global +- io.Writer https://golang.org/search?q=Write#Global + +#### 代表的なもの + +- [package bufio](https://golang.org/search?q=Read#Global_pkg/bufio) +- [package csv](https://golang.org/search?q=Read#Global_pkg/csv) +- os.Stdin +- os.Stdout +- os.File + +## io.Reader と io.Writer があることでどういう利点があるのか具体例を挙げて考えてみる(回答) + +### 1. どこからデータを読み込み、どこへ書き出すかについて自由に実装ができる + +os.Stdin、os.File からの読み込み、os.Stdout, os.File への書き出しなど、Read/Write 関数の引数・戻値が同じのため、呼び出す元の型を変えるだけで、異なる読み出し・書き出し先を実装できる + +### 2. シンプルな構造で、カスタマイズが簡単に実装できる + +引数1つ、戻値が2つしかないシンプルな関数を書くだけでよい。下のコードはアルファベット以外の文字を読み込まないようにカスタマイズした、Reader の例。 + +```golang +type alphaReader struct { + src string + cur int +} + +func newAlphaReader(src string) *alphaReader { + return &alphaReader{src: src} +} + +func alpha(r byte) byte { + if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') { + return r + } + return 0 +} + +func (a *alphaReader) Read(p []byte) (int, error) { + if a.cur >= len(a.src) { + return 0, io.EOF + } + + x := len(a.src) - a.cur + n, bound := 0, 0 + if x >= len(p) { + bound = len(p) + } else if x <= len(p) { + bound = x + } + + buf := make([]byte, bound) + for n < bound { + if char := alpha(a.src[a.cur]); char != 0 { + buf[n] = char + } + n++ + a.cur++ + } + copy(p, buf) + return n, nil +} + +func main() { + reader := newAlphaReader("Hello! It's 9am, where is the sun?") + p := make([]byte, 4) + for { + n, err := reader.Read(p) + if err == io.EOF { + break + } + fmt.Print(string(p[:n])) + } + fmt.Println() +} +``` + +### 3. 既存の Reader/Writer からの拡張が簡単に実装できる + +下は io.Reader を持つ構造体の例。下の main 関数では strings.Reader に 2.での例と同様、アルファベットのみ読み込む機能を拡張しているが、この部分は alphaReader を変更することなく、main 関数内で os.File、bufio.Reader などに変更するだけで実現が可能になる。 + +```golang +type alphaReader struct { + reader io.Reader +} + +func newAlphaReader(reader io.Reader) *alphaReader { + return &alphaReader{reader: reader} +} + +func alpha(r byte) byte { + if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') { + return r + } + return 0 +} + +func (a *alphaReader) Read(p []byte) (int, error) { + n, err := a.reader.Read(p) + if err != nil { + return n, err + } + buf := make([]byte, n) + for i := 0; i < n; i++ { + if char := alpha(p[i]); char != 0 { + buf[i] = char + } + } + + copy(p, buf) + return n, nil +} + +func main() { + // use an io.Reader as source for alphaReader + reader := newAlphaReader(strings.NewReader("Hello! It's 9am, where is the sun?")) + p := make([]byte, 4) + for { + n, err := reader.Read(p) + if err == io.EOF { + break + } + fmt.Print(string(p[:n])) + } + fmt.Println() +} + +``` + +#### 参考文献 + +- https://medium.com/learning-the-go-programming-language/streaming-io-in-go-d93507931185 + +- https://qiita.com/ktnyt/items/8ede94469ba8b1399b12 + +# Try 2: テストを書いてみよう + +## 1 回目の課題のテストを作ってみてください。 + +- テストのしやすさを考えてリファクタリングしてみる +- テストのカバレッジを取ってみる +- テーブル駆動テストを行う +- テストヘルパーを作ってみる + +### 回答 + +以下のコマンドでテストの実行が可能です。今回は 85.2%程カバーすることができました。 + +``` +$ go test --cover ./imgconv --coverprofile cover.out +ok github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv 6.727s coverage: 85.2% of statements +``` + +また、以下でコードのどこがテストされたか、ブラウザに表示をさせて確認をすることができます。 + +``` +$ go tool cover -html=cover.out +``` + +### 感想 + +- t.Helper()は、名前からもっとたくさんの情報を出力してくれるのかと期待してしまいましたが、呼び出し元の位置がわかるだけでした。 +- 小さな関数からテストを書いていくより、親となる関数からテストを書いて、カバーした内容を確認しながら、カバーしきれない分のテストを足していったほうが効率的だとおもいました。 +- err の状況を作り出す方法がわからずに、カバーしきれなかった部分もありました。(例えば下のような箇所)100%のカバレッジを目指すには、こういった点をカバーするために png.Encode でエラーが出るための条件について調査などが必要になります。 + +```golang +switch c.output { + case "png": + e = png.Encode(out, img) + case "jpg": + e = jpeg.Encode(out, img, nil) + } + if e != nil { + return e + } +} +``` + +### 参考文献 + +- カバレッジとは https://www.techmatrix.co.jp/t/quality/coverage.html +- テーブル駆動テストとは https://github.com/golang/go/wiki/TableDrivenTests +- テストヘルパーとは https://qiita.com/atotto/items/f6b8c773264a3183a53c + +``` +testCompute に t.Helper()の1行を追加します。すると、testCompute 内で発生したエラーは呼び元の TestCompute のどの行で失敗したのかを表示するようになります。 +``` diff --git a/kadai2/segakazzz/cover.out b/kadai2/segakazzz/cover.out new file mode 100644 index 0000000..6a49203 --- /dev/null +++ b/kadai2/segakazzz/cover.out @@ -0,0 +1,63 @@ +mode: set +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:26.63,28.16 2 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:32.2,33.16 2 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:37.2,38.12 2 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:28.16,31.3 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:33.16,36.3 1 0 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:41.84,48.15 2 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:54.2,54.16 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:61.2,61.21 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:64.2,64.72 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:49.20,50.33 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:51.10,52.26 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:55.20,56.35 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:57.10,58.27 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:61.21,63.3 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:68.37,70.14 2 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:73.2,74.14 2 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:77.2,77.12 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:70.14,72.3 1 0 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:74.14,76.3 1 0 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:80.61,83.16 3 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:86.2,86.26 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:92.2,92.25 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:83.16,85.3 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:86.26,87.17 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:87.17,89.4 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:95.61,97.14 2 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:100.2,100.29 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:108.2,108.12 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:97.14,99.3 1 0 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:100.29,101.34 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:101.34,103.16 2 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:103.16,105.5 1 0 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:111.62,116.26 5 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:120.2,121.14 2 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:125.2,125.15 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:129.2,130.26 2 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:135.2,135.14 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:139.2,139.15 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:143.2,146.17 2 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:153.2,153.14 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:156.2,156.18 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:162.2,162.14 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:165.2,165.12 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:116.26,118.3 1 0 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:121.14,123.3 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:125.15,127.3 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:130.26,132.3 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:132.8,134.3 1 0 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:135.14,137.3 1 0 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:139.15,141.3 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:147.13,148.27 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:149.13,150.26 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:153.14,155.3 1 0 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:157.13,158.27 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:159.13,160.33 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:162.14,164.3 1 0 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:168.54,170.24 2 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:173.2,173.22 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:170.24,172.3 1 0 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:176.52,178.24 2 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:181.2,181.21 1 1 +github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv/imgconv.go:178.24,180.3 1 0 diff --git a/kadai2/segakazzz/imgconv/imgconv.go b/kadai2/segakazzz/imgconv/imgconv.go new file mode 100644 index 0000000..390e04b --- /dev/null +++ b/kadai2/segakazzz/imgconv/imgconv.go @@ -0,0 +1,182 @@ +// Package imgconv is for Gopher Dojo Kadai1 +package imgconv + +import ( + "errors" + "fmt" + "image" + "image/jpeg" + "image/png" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" +) + +type converter struct { + dirname string + input string + output string +} + +// RunConverter converts all image files in the directory which you indicate with -d option. +// If the process is completed succeessfully, you will see the list of output files and "Done!" +// message in the standard output. +func RunConverter(dir *string, in *string, out *string) error { + c, err := newConverter(*dir, *in, *out) + if err != nil { + // log.Fatal(err) + return err + } + err = c.Convert() + if err != nil { + // log.Fatal(err) + return err + } + fmt.Println("Done!") + return nil +} + +func newConverter(dirname string, input string, output string) (*converter, error) { + var ( + ErrInputExt = errors.New("Input extension is not valid. Select one from jpg/png") + ErrOutputExt = errors.New("Output extension is not valid. Select one from jpg/png") + ErrExtSame = errors.New("Input and Output extensiton is the same. No convertion is needed") + ) + + switch input { + case "jpg", "png": + input = strings.ToLower(input) + default: + return nil, ErrInputExt + } + switch output { + case "jpg", "png": + output = strings.ToLower(output) + default: + return nil, ErrOutputExt + } + + if input == output { + return nil, ErrExtSame + } + return &converter{dirname: dirname, input: input, output: output}, nil +} + +// Convert method converts all jpg files in dirname to png. "out" folder is generated if it doesn't exist. +func (c *converter) Convert() error { + files, e := c.getSourceFiles() + if e != nil { + return e + } + e = c.convertFiles(files) + if e != nil { + return e + } + return nil +} + +func (c *converter) getSourceFiles() ([]os.FileInfo, error) { + var outputFiles []os.FileInfo + files, err := ioutil.ReadDir(c.dirname) + if err != nil { + return nil, err + } + for _, f := range files { + if !f.IsDir() { + outputFiles = append(outputFiles, f) + } + } + + return outputFiles, nil +} + +func (c *converter) convertFiles(files []os.FileInfo) error { + re, e := regexp.Compile("." + c.input + "$") + if e != nil { + return e + } + for _, file := range files { + if re.MatchString(file.Name()) { + e = c.convertSingle(file.Name()) + if e != nil { + return e + } + } + } + return nil +} + +func (c *converter) convertSingle(filename string) (e error) { + input := filepath.Join(c.dirname, filename) + outDir := filepath.Join(c.dirname, "out") + output := filepath.Join(outDir, strings.Replace(strings.ToLower(filename), "."+c.input, "."+c.output, -1)) + fmt.Println(output) + if !c.dirExists(outDir) { + os.Mkdir(outDir, 0755) + } + + in, e := os.Open(input) + if e != nil { + return e + } + + defer func() { + e = in.Close() + }() + + var out *os.File + if c.fileExists(output) { + out, e = os.OpenFile(output, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + } else { + out, e = os.Create(output) + } + if e != nil { + return e + } + + defer func() { + e = out.Close() + }() + + var ( + img image.Image + ) + switch c.input { + case "jpg": + img, e = jpeg.Decode(in) + case "png": + img, e = png.Decode(in) + } + + if e != nil { + return e + } + switch c.output { + case "png": + e = png.Encode(out, img) + case "jpg": + e = jpeg.Encode(out, img, nil) + } + if e != nil { + return e + } + return nil +} + +func (c *converter) fileExists(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} + +func (c *converter) dirExists(dirname string) bool { + info, err := os.Stat(dirname) + if os.IsNotExist(err) { + return false + } + return info.IsDir() +} diff --git a/kadai2/segakazzz/imgconv/imgconv_test.go b/kadai2/segakazzz/imgconv/imgconv_test.go new file mode 100644 index 0000000..8a12cfe --- /dev/null +++ b/kadai2/segakazzz/imgconv/imgconv_test.go @@ -0,0 +1,151 @@ +package imgconv + +import ( + "testing" +) + +type pattern struct { + dirname string + input string + output string + expected *converter + isError bool +} + +func TestRunConverter(t *testing.T) { + t.Helper() + t.Run("Success", func(t *testing.T) { + dir, input, output := "../testdata", "jpg", "png" + testRunConverter(t, &dir, &input, &output, false) + }) + t.Run("Error", func(t *testing.T) { + dir, input, output := "../testdata", "gif", "png" + testRunConverter(t, &dir, &input, &output, true) + }) +} + +func testRunConverter(t *testing.T, dir *string, input *string, output *string, isError bool) { + t.Helper() + if err := RunConverter(dir, input, output); isError && err == nil { + t.Fatalf("Error is expected but not found") + } else if !isError && err != nil { + t.Fatalf("Error is not expected but found %s", err) + } +} + +func TestNewConverter(t *testing.T) { + successPatterns := []pattern{ + {"testdata", "png", "jpg", &converter{dirname: "testdata", input: "png", output: "jpg"}, false}, + {"testdata", "jpg", "png", &converter{dirname: "testdata", input: "jpg", output: "png"}, false}, + } + errorPatterns := []pattern{ + {"testdata", "jpg", "gif", nil, true}, + {"testdata", "gif", "jpg", nil, true}, + {"testdata", "jpg", "jpg", nil, true}, + } + + t.Run("successPatterns", func(t *testing.T) { + for i, p := range successPatterns { + testNewConverter(t, i, p) + } + }) + + t.Run("errorPatterns", func(t *testing.T) { + for i, p := range errorPatterns { + testNewConverter(t, i, p) + } + }) +} + +func testNewConverter(t *testing.T, i int, p pattern) { + t.Helper() + actual, err := newConverter(p.dirname, p.input, p.output) + if err != nil && p.isError == false { + t.Fatalf("pattern[%d]: %v want: NO ERROR, actual: %v", i, p, err) + } + if err == nil && p.isError == true { + t.Fatalf("pattern[%d]: %v want: ERROR, actual: NO ERROR", i, p) + } + if (actual == nil && p.expected != nil) || (actual != nil && p.expected == nil) { + t.Fatalf("pattern[%d]: %v want: isNil(%t), actual: isNil(%t)", i, p, p.expected == nil, actual == nil) + } + + if actual != nil && p.expected != nil && *actual != *p.expected { + t.Fatalf("pattern[%d]: %v want: %v [%T], actual: %v [%T]", i, p, *p.expected, *p.expected, *actual, *actual) + } + +} + +func TestGetSourceFiles(t *testing.T) { + t.Run("Dir Exists", func(t *testing.T) { + testGetSourceFiles(t, &converter{dirname: "../testdata/test/4", input: "jpg", output: "png"}, 4, false) + testGetSourceFiles(t, &converter{dirname: "../testdata/test/2", input: "jpg", output: "png"}, 2, false) + }) + t.Run("Dir Not Exists", func(t *testing.T) { + testGetSourceFiles(t, &converter{dirname: "../notfound", input: "jpg", output: "png"}, 0, true) + }) +} + +func testGetSourceFiles(t *testing.T, c *converter, expected int, isError bool) { + t.Helper() + files, err := c.getSourceFiles() + if isError && err == nil { + t.Fatalf("converter[%v] Error is expected. But not found", *c) + } + if !isError && err != nil { + t.Fatalf("Error is not expected but found %s", err) + } + if actual := len(files); actual != expected { + t.Fatalf("converter[%v] File count is not expected. expected: %d actual: %d", *c, expected, actual) + } +} + +func TestConvertSingle(t *testing.T) { + t.Run("File Found", func(t *testing.T) { + testConvertSingle(t, &converter{dirname: "../testdata", input: "jpg", output: "png"}, "001.jpg", false) + testConvertSingle(t, &converter{dirname: "../testdata", input: "png", output: "jpg"}, "001.png", false) + }) + t.Run("Output folder exists", func(t *testing.T) { + testConvertSingle(t, &converter{dirname: "../testdata", input: "jpg", output: "png"}, "001.jpg", false) + testConvertSingle(t, &converter{dirname: "../testdata/test/2", input: "png", output: "jpg"}, "001.png", false) + }) + t.Run("File Not Found", func(t *testing.T) { + testConvertSingle(t, &converter{dirname: "../testdata", input: "jpg", output: "png"}, "111.jpg", true) + }) + +} + +func testConvertSingle(t *testing.T, c *converter, fname string, isError bool) { + t.Helper() + err := c.convertSingle(fname) + if err == nil && isError { + t.Fatalf("pattern[%v] Error is expected but not found", *c) + } + if err != nil && !isError { + t.Fatalf("pattern[%v] Error is not expected but found %s", *c, err) + } +} + +func TestConvertFiles(t *testing.T) { + t.Helper() + t.Run("Success", func(t *testing.T) { + testConvertFiles(t, &converter{dirname: "../testdata", input: "png", output: "jpg"}, false) + }) + // t.Run("Error", func(t *testing.T) { + // testConvertFiles(t, &converter{dirname: "../testdata", input: "gif", output: "jpg"}, true) + // }) +} + +func testConvertFiles(t *testing.T, c *converter, isError bool) { + t.Helper() + files, err := c.getSourceFiles() + if err != nil { + t.Fatalf("error found %s", err) + } + if err := c.convertFiles(files); isError && err == nil { + t.Fatalf("pattern[%v] Error is expected but not found", c) + } else if !isError && err != nil { + t.Fatalf("pattern[%v] Error is not expected but found %s", c, err) + } + +} diff --git a/kadai2/segakazzz/main.go b/kadai2/segakazzz/main.go new file mode 100644 index 0000000..c6ec770 --- /dev/null +++ b/kadai2/segakazzz/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "flag" + "os" + + "github.com/gopherdojo/dojo8/kadai2/segakazzz/imgconv" +) + +func main() { + var ( + dir = flag.String("d", ".", "Indicate directory to convert") + in = flag.String("i", "jpg", "Indicate input image file's extension") + out = flag.String("o", "png", "Indicate output image file's extension") + ) + + flag.Parse() + err := imgconv.RunConverter(dir, in, out) + if err != nil { + os.Exit(1) + } + os.Exit(0) +} diff --git a/kadai2/segakazzz/testdata/download.zsh b/kadai2/segakazzz/testdata/download.zsh new file mode 100755 index 0000000..e9fd9f0 --- /dev/null +++ b/kadai2/segakazzz/testdata/download.zsh @@ -0,0 +1,12 @@ +#!/bin/zsh + +curl https://cdn2.thecatapi.com/images/8er.jpg > 001.jpg +curl https://cdn2.thecatapi.com/images/ceh.jpg > 002.jpg +curl https://cdn2.thecatapi.com/images/MTU2MjQ4NA.jpg > 003.jpg +curl https://cdn2.thecatapi.com/images/5qc.jpg > 004.jpg +curl https://cdn2.thecatapi.com/images/962.jpg > 005.jpg +curl https://cdn2.thecatapi.com/images/MTc2Mzc0Mw.jpg > 006.jpg +curl https://cdn2.thecatapi.com/images/bos.jpg > 007.jpg +curl https://cdn2.thecatapi.com/images/SX2DvLw7u.jpg > 008.jpg +curl https://cdn2.thecatapi.com/images/9eh.jpg > 009.jpg +curl https://cdn2.thecatapi.com/images/7bo.jpg > 010.jpg \ No newline at end of file