diff --git a/kadai1/torotake/.gitignore b/kadai1/torotake/.gitignore new file mode 100644 index 0000000..dbe9c82 --- /dev/null +++ b/kadai1/torotake/.gitignore @@ -0,0 +1 @@ +.vscode/ \ No newline at end of file diff --git a/kadai1/torotake/README.md b/kadai1/torotake/README.md new file mode 100644 index 0000000..276e0b5 --- /dev/null +++ b/kadai1/torotake/README.md @@ -0,0 +1,38 @@ +# Gopher道場#6 課題1 + +## 次の仕様を満たすコマンドを作って下さい +- [x] ディレクトリを指定する +- [x] 指定したディレクトリ以下のJPGファイルをPNGに変換(デフォルト) +- [x] ディレクトリ以下は再帰的に処理する +- [x] 変換前と変換後の画像形式を指定できる(オプション) + +## 以下を満たすように開発してください +- [x] mainパッケージと分離する +- [x] 自作パッケージと標準パッケージと準標準パッケージのみ使う +- [x] 準標準パッケージ:golang.org/x以下のパッケージ +- [x] ユーザ定義型を作ってみる +- [x] GoDocを生成してみる + +---- + +## ビルド + +```sh +go build imgconv.go +``` + +## Usage + +```sh +imgconv [directory] + +options +-i format : input image format "jpg"(default), "png", "gif", "bmp" +-o format : output image format "jpg", "png"(default), "gif", "bmp" +``` + +---- + +## 反省 +* ユーザー定義型を意味ある形で使えている気がしません +* テストまで書けませんでした… diff --git a/kadai1/torotake/convert/convert.go b/kadai1/torotake/convert/convert.go new file mode 100644 index 0000000..8dc52bc --- /dev/null +++ b/kadai1/torotake/convert/convert.go @@ -0,0 +1,103 @@ +/* +Package convert は指定のディレクトリ以下に存在する指定された形式の画像ファイルを +別の形式の画像ファイルに変換するためのパッケージです。 +*/ +package convert + +import ( + "errors" + "fmt" + "image" + "image/gif" + "image/jpeg" + "image/png" + "os" + "path/filepath" + + "golang.org/x/image/bmp" +) + +// Format 画像ファイルの形式 +type Format int + +const ( + // UNKNOWN 形式不明 + UNKNOWN Format = iota - 1 + // JPEG JPEG形式 + JPEG + // PNG PNG形式 + PNG + // GIF GIF形式 + GIF + // BMP BMP形式 + BMP +) + +// Options 画像変換のオプション指定 +type Options struct { + SrcFiles []string + OutputFormat Format +} + +// Convert optionsに指定された内容に従って画像を変換します +func Convert(options Options) { + for _, src := range options.SrcFiles { + dst := getDst(src, options.OutputFormat) + fmt.Printf("convert %s -> %s ...\n", src, dst) + err := convertFormat(src, dst, options.OutputFormat) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + } + } +} + +func getDst(src string, f Format) string { + return src[:len(src)-len(filepath.Ext(src))] + getDstExt(f) +} + +func getDstExt(f Format) string { + switch f { + case JPEG: + return ".jpg" + case PNG: + return ".png" + case GIF: + return ".gif" + case BMP: + return ".bmp" + } + + return "" +} + +func convertFormat(src string, dst string, format Format) error { + srcFp, err := os.Open(src) + if err != nil { + return err + } + defer srcFp.Close() + + img, _, err := image.Decode(srcFp) + if err != nil { + return err + } + + dstFp, err := os.Create(dst) + if err != nil { + return err + } + defer dstFp.Close() + + switch format { + case JPEG: + return jpeg.Encode(dstFp, img, nil) + case PNG: + return png.Encode(dstFp, img) + case GIF: + return gif.Encode(dstFp, img, nil) + case BMP: + return bmp.Encode(dstFp, img) + } + + return errors.New("Invalid format") +} diff --git a/kadai1/torotake/convert/files.go b/kadai1/torotake/convert/files.go new file mode 100644 index 0000000..0410279 --- /dev/null +++ b/kadai1/torotake/convert/files.go @@ -0,0 +1,61 @@ +package convert + +import ( + "errors" + "os" + "path/filepath" + "strings" +) + +// ListFiles 指定のディレクトリ(dir)以下を再帰的に探索し、指定フォーマット(format)の画像ファイルを列挙します。 +func ListFiles(dir string, format Format) ([]string, error) { + if !isDir(dir) { + return nil, errors.New(dir + " is not directory.") + } + + var files []string + filepath.Walk(dir, generateWalkFunc(&files, format)) + + return files, nil +} + +func isDir(path string) bool { + info, err := os.Stat(path) + if err != nil { + return false + } + + return info.IsDir() +} + +func generateWalkFunc(files *[]string, format Format) func(string, os.FileInfo, error) error { + targetExts := getTargetExts(format) + return func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + return nil + } + + ext := strings.ToLower(filepath.Ext(path)) + for _, v := range targetExts { + if v == ext { + *files = append(*files, path) + } + } + return nil + } +} + +func getTargetExts(format Format) []string { + switch format { + case JPEG: + return []string{".jpeg", ".jpg"} + case PNG: + return []string{".png"} + case GIF: + return []string{".gif"} + case BMP: + return []string{".bmp"} + } + + return []string{} +} diff --git a/kadai1/torotake/imgconv.go b/kadai1/torotake/imgconv.go new file mode 100644 index 0000000..5de6ab4 --- /dev/null +++ b/kadai1/torotake/imgconv.go @@ -0,0 +1,74 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/gopherdojo/dojo6/kadai1/torotake/convert" +) + +var ( + optInputFormat string + optOutputFormat string + exitCode int +) + +func init() { + // -i=[変換前形式] default : jpeg + // -o=[変換後形式] default : png + // 変換対象ディレクトリ + flag.StringVar(&optInputFormat, "i", "jpeg", "input file format.") + flag.StringVar(&optOutputFormat, "o", "png", "output file format.") + flag.Parse() +} + +func main() { + exec() + os.Exit(exitCode) +} + +func exec() { + args := flag.Args() + inputFormat := getFormat(optInputFormat) + outputFormat := getFormat(optOutputFormat) + + if len(args) == 0 { + fmt.Fprintf(os.Stderr, "%s\n", "option error : no target directory") + exitCode = 1 + return + } + + if inputFormat == convert.UNKNOWN || outputFormat == convert.UNKNOWN || inputFormat == outputFormat { + fmt.Fprintf(os.Stderr, "%s\n", "option error : invalid format") + exitCode = 1 + return + } + + files, err := convert.ListFiles(args[0], inputFormat) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + exitCode = 2 + return + } + + convert.Convert(convert.Options{ + SrcFiles: files, + OutputFormat: outputFormat}) +} + +func getFormat(f string) convert.Format { + f = strings.ToLower(f) + if f == "jpg" || f == "jpeg" { + return convert.JPEG + } else if f == "png" { + return convert.PNG + } else if f == "gif" { + return convert.GIF + } else if f == "bmp" { + return convert.BMP + } + + return convert.UNKNOWN +} diff --git a/kadai2/.gitkeep b/kadai2/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/kadai2/torotake/.gitignore b/kadai2/torotake/.gitignore new file mode 100644 index 0000000..74c8c84 --- /dev/null +++ b/kadai2/torotake/.gitignore @@ -0,0 +1,2 @@ +.vscode/ +profile \ No newline at end of file diff --git a/kadai2/torotake/README.md b/kadai2/torotake/README.md new file mode 100644 index 0000000..a12cb68 --- /dev/null +++ b/kadai2/torotake/README.md @@ -0,0 +1,76 @@ +# Gopher道場#6 課題2 + +## io.Readerとio.Writerについて調べてみよう + +### 標準パッケージでどのように使われているか + +* 標準入力、標準出力、標準エラー出力 (osパッケージ) +* 暗号や圧縮などデータ変換ソース (crypt, gzipパッケージなど) +* ネットワーク通信の送受信 (netパッケージなど) +* 各種Reader/WriterをラッピングしてI/O処理にバッファリング機能を追加 (bufioパッケージ) + + +### io.Readerとio.Writerがあることでどういう利点があるのか具体例を挙げて考えてみる + +ファイル、標準入出力、ネットワーク通信など様々なIO処理について、同じ取り扱う事が出来、入出力先の切り替えも容易となる。 + +例えば、fmt.Fprintfでは出力対象としてio.Writerを要求するので渡すものを変えるだけでファイルへの書き込み、標準出力への出力、ネットワークソケットへの送信処理等を行う事が可能。 + +```go +// 標準出力 +fmt.Fprintf(os.Stderr, "Hello World.") + +// ファイルへ書き込み +fp := os.Open("hoge.txt") +fmt.Fprintf(fp, "Hello World.") + +// ネットワークへ送信 +conn, _ := net.Dial("tcp", "example.com:80") +fmt.Fprintf(conn, "Hello World.") +``` + +---- + +## 1回目の宿題のテストを作ってみて下さい + +- [ ] テストのしやすさを考えてリファクタリングしてみる +- [x] テストのカバレッジを取ってみる +- [x] テーブル駆動テストを行う +- [ ] テストヘルパーを作ってみる + +テスト実行 +``` +$ go test -v github.com/gopherdojo/dojo6/kadai2/torotake/convert + +=== RUN TestConvert +=== RUN TestConvert/ConvertJpegToPng +convert testdata/test/test_jpg.jpg -> testdata/test/test_jpg.png ... +=== RUN TestConvert/ConvertJpegToBmp +convert testdata/test/test_jpg.jpg -> testdata/test/test_jpg.bmp ... +=== RUN TestConvert/ConvertJpegToGif +convert testdata/test/test_jpg.jpg -> testdata/test/test_jpg.gif ... +=== RUN TestConvert/ConvertPngToJpeg +convert testdata/test/test_png.png -> testdata/test/test_png.jpg ... +=== RUN TestConvert/ConvertPngToBmp +convert testdata/test/test_png.png -> testdata/test/test_png.bmp ... +=== RUN TestConvert/ConvertPngToGif +convert testdata/test/test_png.png -> testdata/test/test_png.gif ... +--- PASS: TestConvert (1.77s) + --- PASS: TestConvert/ConvertJpegToPng (0.32s) + --- PASS: TestConvert/ConvertJpegToBmp (0.04s) + --- PASS: TestConvert/ConvertJpegToGif (0.70s) + --- PASS: TestConvert/ConvertPngToJpeg (0.06s) + --- PASS: TestConvert/ConvertPngToBmp (0.03s) + --- PASS: TestConvert/ConvertPngToGif (0.59s) +=== RUN TestListFiles +--- PASS: TestListFiles (0.00s) +PASS +ok github.com/gopherdojo/dojo6/kadai2/torotake/convert 1.776s +``` + +カバレッジ +``` +$ go test -coverprofile=profile github.com/gopherdojo/dojo6/kadai2/torotake/convert + +ok github.com/gopherdojo/dojo6/kadai2/torotake/convert 1.771s coverage: 77.8% of statements +``` diff --git a/kadai2/torotake/convert/convert.go b/kadai2/torotake/convert/convert.go new file mode 100644 index 0000000..8dc52bc --- /dev/null +++ b/kadai2/torotake/convert/convert.go @@ -0,0 +1,103 @@ +/* +Package convert は指定のディレクトリ以下に存在する指定された形式の画像ファイルを +別の形式の画像ファイルに変換するためのパッケージです。 +*/ +package convert + +import ( + "errors" + "fmt" + "image" + "image/gif" + "image/jpeg" + "image/png" + "os" + "path/filepath" + + "golang.org/x/image/bmp" +) + +// Format 画像ファイルの形式 +type Format int + +const ( + // UNKNOWN 形式不明 + UNKNOWN Format = iota - 1 + // JPEG JPEG形式 + JPEG + // PNG PNG形式 + PNG + // GIF GIF形式 + GIF + // BMP BMP形式 + BMP +) + +// Options 画像変換のオプション指定 +type Options struct { + SrcFiles []string + OutputFormat Format +} + +// Convert optionsに指定された内容に従って画像を変換します +func Convert(options Options) { + for _, src := range options.SrcFiles { + dst := getDst(src, options.OutputFormat) + fmt.Printf("convert %s -> %s ...\n", src, dst) + err := convertFormat(src, dst, options.OutputFormat) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + } + } +} + +func getDst(src string, f Format) string { + return src[:len(src)-len(filepath.Ext(src))] + getDstExt(f) +} + +func getDstExt(f Format) string { + switch f { + case JPEG: + return ".jpg" + case PNG: + return ".png" + case GIF: + return ".gif" + case BMP: + return ".bmp" + } + + return "" +} + +func convertFormat(src string, dst string, format Format) error { + srcFp, err := os.Open(src) + if err != nil { + return err + } + defer srcFp.Close() + + img, _, err := image.Decode(srcFp) + if err != nil { + return err + } + + dstFp, err := os.Create(dst) + if err != nil { + return err + } + defer dstFp.Close() + + switch format { + case JPEG: + return jpeg.Encode(dstFp, img, nil) + case PNG: + return png.Encode(dstFp, img) + case GIF: + return gif.Encode(dstFp, img, nil) + case BMP: + return bmp.Encode(dstFp, img) + } + + return errors.New("Invalid format") +} diff --git a/kadai2/torotake/convert/convert_test.go b/kadai2/torotake/convert/convert_test.go new file mode 100644 index 0000000..ec60e47 --- /dev/null +++ b/kadai2/torotake/convert/convert_test.go @@ -0,0 +1,108 @@ +package convert_test + +import ( + "io" + "os" + + "testing" + + "github.com/gopherdojo/dojo6/kadai2/torotake/convert" +) + +func setupTestConvert(t *testing.T) func(t *testing.T) { + // テスト用のディレクトリを作成し、テストデータをコピーする + if err := os.Mkdir("testdata/test", 0777); err != nil { + t.Fatalf("setup failed : can't create test dir") + } + copyFile("testdata/test_jpg.jpg", "testdata/test/test_jpg.jpg") + copyFile("testdata/test_png.png", "testdata/test/test_png.png") + copyFile("testdata/test_bmp.bmp", "testdata/test/test_bmp.bmp") + copyFile("testdata/test_gif.gif", "testdata/test/test_gif.gif") + + // teardown テスト用のディレクトリを削除する + return func(t *testing.T) { + if err := os.RemoveAll("testdata/test"); err != nil { + t.Errorf("teardown failed : can't remove test dir") + } + } +} + +// TestConvert convert.Convert()の指定のフォーマットへの画像変換をテスト +func TestConvert(t *testing.T) { + teardownTestConvert := setupTestConvert(t) + defer teardownTestConvert(t) + + var cases = []struct { + name string + src []string + format convert.Format + actual string + }{ + { + "ConvertJpegToPng", + []string{"testdata/test/test_jpg.jpg"}, + convert.PNG, + "testdata/test/test_jpg.png", + }, + { + "ConvertJpegToBmp", + []string{"testdata/test/test_jpg.jpg"}, + convert.BMP, + "testdata/test/test_jpg.bmp", + }, + { + "ConvertJpegToGif", + []string{"testdata/test/test_jpg.jpg"}, + convert.GIF, + "testdata/test/test_jpg.gif", + }, + { + "ConvertPngToJpeg", + []string{"testdata/test/test_png.png"}, + convert.JPEG, + "testdata/test/test_png.jpg", + }, + { + "ConvertPngToBmp", + []string{"testdata/test/test_png.png"}, + convert.BMP, + "testdata/test/test_png.bmp", + }, + { + "ConvertPngToGif", + []string{"testdata/test/test_png.png"}, + convert.GIF, + "testdata/test/test_png.gif", + }, + } + + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + t.Helper() + opts := convert.Options{SrcFiles: c.src, OutputFormat: c.format} + convert.Convert(opts) + _, err := os.Stat(c.actual) + if err != nil { + t.Fatalf("%v is not found", c.actual) + } + }) + } +} + +func copyFile(src string, dst string) error { + srcFp, err := os.Open(src) + if err != nil { + return err + } + defer srcFp.Close() + + dstFp, err := os.Create(dst) + if err != nil { + return err + } + defer dstFp.Close() + + _, err = io.Copy(dstFp, srcFp) + return err +} diff --git a/kadai2/torotake/convert/files.go b/kadai2/torotake/convert/files.go new file mode 100644 index 0000000..0410279 --- /dev/null +++ b/kadai2/torotake/convert/files.go @@ -0,0 +1,61 @@ +package convert + +import ( + "errors" + "os" + "path/filepath" + "strings" +) + +// ListFiles 指定のディレクトリ(dir)以下を再帰的に探索し、指定フォーマット(format)の画像ファイルを列挙します。 +func ListFiles(dir string, format Format) ([]string, error) { + if !isDir(dir) { + return nil, errors.New(dir + " is not directory.") + } + + var files []string + filepath.Walk(dir, generateWalkFunc(&files, format)) + + return files, nil +} + +func isDir(path string) bool { + info, err := os.Stat(path) + if err != nil { + return false + } + + return info.IsDir() +} + +func generateWalkFunc(files *[]string, format Format) func(string, os.FileInfo, error) error { + targetExts := getTargetExts(format) + return func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + return nil + } + + ext := strings.ToLower(filepath.Ext(path)) + for _, v := range targetExts { + if v == ext { + *files = append(*files, path) + } + } + return nil + } +} + +func getTargetExts(format Format) []string { + switch format { + case JPEG: + return []string{".jpeg", ".jpg"} + case PNG: + return []string{".png"} + case GIF: + return []string{".gif"} + case BMP: + return []string{".bmp"} + } + + return []string{} +} diff --git a/kadai2/torotake/convert/files_test.go b/kadai2/torotake/convert/files_test.go new file mode 100644 index 0000000..175bbc8 --- /dev/null +++ b/kadai2/torotake/convert/files_test.go @@ -0,0 +1,18 @@ +package convert_test + +import ( + "testing" + + "github.com/gopherdojo/dojo6/kadai2/torotake/convert" +) + +// convert.ListFiles()が指定のフォーマットのファイルを列挙出来るかをテスト +func TestListFiles(t *testing.T) { + files, err := convert.ListFiles("./testdata", convert.JPEG) + if err != nil { + t.Errorf("ListFiles returns err %#v", err) + } + if len(files) != 1 || files[0] != "testdata/test_jpg.jpg" { + t.Errorf("ListFiles JPEG : failed") + } +} diff --git a/kadai2/torotake/convert/testdata/test_bmp.bmp b/kadai2/torotake/convert/testdata/test_bmp.bmp new file mode 100644 index 0000000..955a7a5 Binary files /dev/null and b/kadai2/torotake/convert/testdata/test_bmp.bmp differ diff --git a/kadai2/torotake/convert/testdata/test_gif.gif b/kadai2/torotake/convert/testdata/test_gif.gif new file mode 100644 index 0000000..b62765c Binary files /dev/null and b/kadai2/torotake/convert/testdata/test_gif.gif differ diff --git a/kadai2/torotake/convert/testdata/test_jpg.jpg b/kadai2/torotake/convert/testdata/test_jpg.jpg new file mode 100644 index 0000000..100bf0b Binary files /dev/null and b/kadai2/torotake/convert/testdata/test_jpg.jpg differ diff --git a/kadai2/torotake/convert/testdata/test_png.png b/kadai2/torotake/convert/testdata/test_png.png new file mode 100644 index 0000000..1d49f07 Binary files /dev/null and b/kadai2/torotake/convert/testdata/test_png.png differ diff --git a/kadai2/torotake/imgconv.go b/kadai2/torotake/imgconv.go new file mode 100644 index 0000000..48d062e --- /dev/null +++ b/kadai2/torotake/imgconv.go @@ -0,0 +1,74 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/gopherdojo/dojo6/kadai2/torotake/convert" +) + +var ( + optInputFormat string + optOutputFormat string + exitCode int +) + +func init() { + // -i=[変換前形式] default : jpeg + // -o=[変換後形式] default : png + // 変換対象ディレクトリ + flag.StringVar(&optInputFormat, "i", "jpeg", "input file format.") + flag.StringVar(&optOutputFormat, "o", "png", "output file format.") + flag.Parse() +} + +func main() { + exec() + os.Exit(exitCode) +} + +func exec() { + args := flag.Args() + inputFormat := getFormat(optInputFormat) + outputFormat := getFormat(optOutputFormat) + + if len(args) == 0 { + fmt.Fprintf(os.Stderr, "%s\n", "option error : no target directory") + exitCode = 1 + return + } + + if inputFormat == convert.UNKNOWN || outputFormat == convert.UNKNOWN || inputFormat == outputFormat { + fmt.Fprintf(os.Stderr, "%s\n", "option error : invalid format") + exitCode = 1 + return + } + + files, err := convert.ListFiles(args[0], inputFormat) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + exitCode = 2 + return + } + + convert.Convert(convert.Options{ + SrcFiles: files, + OutputFormat: outputFormat}) +} + +func getFormat(f string) convert.Format { + f = strings.ToLower(f) + if f == "jpg" || f == "jpeg" { + return convert.JPEG + } else if f == "png" { + return convert.PNG + } else if f == "gif" { + return convert.GIF + } else if f == "bmp" { + return convert.BMP + } + + return convert.UNKNOWN +}