Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hokita / 課題1 #5

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions kadai1/hokita/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 課題 1 画像変換コマンドを作ろう

## 課題内容
### 次の仕様を満たすコマンドを作って下さい

- ディレクトリを指定する
- 指定したディレクトリ以下の JPG ファイルを PNG に変換(デフォルト)
- ディレクトリ以下は再帰的に処理する
- 変換前と変換後の画像形式を指定できる(オプション)

### 以下を満たすように開発してください

- main パッケージと分離する
- 自作パッケージと標準パッケージと準標準パッケージのみ使う
- 準標準パッケージ:golang.org/x 以下のパッケージ
- ユーザ定義型を作ってみる
- GoDoc を生成してみる
- Go Modules を使ってみる

## 対応したこと
- 画像を変換
- 現状はjpg, pngのみ
- jpg, png以外はエラー表示
- 画像出力先は対象画像と同じディレクトリ
- 指定したディレクトリが無いとエラーを表示

## 動作
```shell
$ go build -o test_imgconv

$ ./test_imgconv -h
Usage of ./test_imgconv:
-from string
Conversion source extension. (default "jpg")
-to string
Conversion target extension. (default "png")

# testdata内のすべてのjpgファイルをpngに変換する
$ ./test_imgconv testdata
Conversion finished!

# testdata内のすべてのpngファイルをjpgに変換する
$ ./test_imgconv -from png -to jpg testdata
Conversion finished!

# ディレクトリの指定が無い場合はエラー
$ ./test_imgconv
Please specify a directory.

# 存在しないディレクトリの場合はエラー
$ ./test_imgconv non_exist_dir
Cannot find directory.

# 対応していない拡張子の場合はエラー
$ ./test_imgconv -from txt -to jpg testdata
Selected extension is not supported.
```

## 工夫したこと
- png, jpg以外にも拡張子が増えそうなので、`image_type`というinterfaceを作ってみた。
- 拡張子の微妙な違い(jpg, jpeg, JPGなど)にも対応できるようにした。

## わからなかったこと、むずかしかったこと
- go mod initで指定するmodule名に命名規則があるのか。
- 普段オブジェクト指向(その上動的型付け言語)で書いているので、それがgoらしいコードになっているのか不安。
- なんでもかんでも構造体メソッドにしたい願望がでてくる
3 changes: 3 additions & 0 deletions kadai1/hokita/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module imgconv

go 1.14
Empty file added kadai1/hokita/go.sum
Empty file.
64 changes: 64 additions & 0 deletions kadai1/hokita/imgconv/converter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package imgconv

import (
"image"
"os"
"path/filepath"
)

func converterFactory(from string, to string) (*Converter, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from, to stringと書けます

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

型で十分伝わるので名前はnewで十分かなと思います

fromImage, err := selectImage("." + from)
if err != nil {
return nil, err
}

toImage, err := selectImage("." + to)
if err != nil {
return nil, err
}

return &Converter{fromImage, toImage}, nil
}

type Converter struct {
fromImage ImageType
toImage ImageType
}

func (conv *Converter) Execute(path string) error {
// ignore unrelated file
if !conv.fromImage.IsMatchExt(filepath.Ext(path)) {
return nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これだと成功したのか失敗したのか無視されたのか分かりづらいので、エラーを返して、呼び出し元で判断してもらうのが良さそうですね。たとえば、os.IsNotExist関数とかみたいにエラーを判定する関数があると良いかも知れないです。
(この辺はエラー処理の章でやります!)

}

// file open
file, err := os.Open(path)
defer file.Close()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defer Closeはエラー処理のあと

if err != nil {
return err
}

// convert to image obj
img, _, err := image.Decode(file)
if err != nil {
return err
}

// output file
out, err := os.Create(conv.SwitchExt(path))
defer out.Close()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

os.Createのエラー処理の後で、defer文でCloseをしてください。
Closeメソッドもエラーを返すので、書き込みの場合のみエラー処理をお願いします。
コマンドラインツールの章のスライドにやり方は書いてあるので確認してみてください!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

書き込みの場合では Close がエラーを返す可能性があるんですね。
勉強になりました🙇

if err != nil {
return err
}

// output image
conv.toImage.Encode(out, img)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

エンコードするのにエラーが起きることはない?

return nil
}

func (conv *Converter) SwitchExt(path string) string {
ext := filepath.Ext(path)
toExt := conv.toImage.Extensions()[0]

return path[:len(path)-len(ext)] + toExt
}
26 changes: 26 additions & 0 deletions kadai1/hokita/imgconv/image_type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package imgconv

import (
"errors"
"image"
"io"
)

type ImageType interface {
Encode(w io.Writer, m image.Image) error
IsMatchExt(ext string) bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hasとかぐらいでいい気もしますね。
pngImage.Has(ext)とかになると思うので。
長くてHasExtとか。

Extensions() []string
}

func selectImage(ext string) (ImageType, error) {
pngImage := PngImage{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

フィールドの初期化を伴わない構造体リテラル(コンポジットリテラル)での初期化は不要です。
構造体もゼロ値で初期化されるため、var pngImg PngImageのように変数を定義されば十分です。

jpegImage := JpegImage{}

if pngImage.IsMatchExt(ext) {
return pngImage, nil
} else if jpegImage.IsMatchExt(ext) {
return jpegImage, nil
}

return nil, errors.New("Selected extension is not supported.")
}
41 changes: 41 additions & 0 deletions kadai1/hokita/imgconv/imgconv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package imgconv

import (
"errors"
"flag"
"os"
"path/filepath"
)

func Call() error {
var (
from = flag.String("from", "jpg", "Conversion source extension.")
to = flag.String("to", "png", "Conversion target extension.")
)
flag.Parse()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mainパッケージ以外でflagパッケージに依存させない(明示的に*flag.FlagSetを保つ場合を除く)。
flagパッケージやos.Argsに依存すると、コマンドラインツールありきのパッケージになってしまいます。

dir := flag.Arg(0)

if flag.Arg(0) == "" {
return errors.New("Please specify a directory.")
}

if f, err := os.Stat(dir); os.IsNotExist(err) || !f.IsDir() {
return errors.New("Cannot find directory.")
}

converter, err := converterFactory(*from, *to)
if err != nil {
return err
}

err = filepath.Walk(dir,
func(path string, info os.FileInfo, err error) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

引数のエラーのエラー処理。

err = converter.Execute(path)
return err
})
if err != nil {
return err
}

return nil
}
29 changes: 29 additions & 0 deletions kadai1/hokita/imgconv/jpeg_image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package imgconv

import (
"image"
"image/jpeg"
"io"
)

const QUALITY = 100
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

定数であってもキャメルケースで書きます。
先頭が大文字だとパッケージ外に公開されます。

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JPEGのQUALITYっていうのが分かるようにしたほうが良さそうですね。


type JpegImage struct{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JPEGで十分だと思います。Goでは略語はすべて大文字か全部小文字にします。


func (_ JpegImage) Encode(w io.Writer, m image.Image) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

レシーバを参照しない場合は_も省略できます。

err := jpeg.Encode(w, m, &jpeg.Options{Quality: QUALITY})
return err
}

func (ji JpegImage) IsMatchExt(ext string) bool {
for _, myExt := range ji.Extensions() {
if ext == myExt {
return true
}
}
return false
}

func (_ JpegImage) Extensions() []string {
return []string{".jpg", ".jpeg", ".JPG", ".JPEG"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mapの方がいいのと、メソッドを呼び出すたびに生成する必要はないのでパッケージ変数にしても良さそうです

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

以下同様です。

}
27 changes: 27 additions & 0 deletions kadai1/hokita/imgconv/png_image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package imgconv

import (
"image"
"image/png"
"io"
)

type PngImage struct{}

func (_ PngImage) Encode(w io.Writer, m image.Image) error {
err := png.Encode(w, m)
return err
}

func (pi PngImage) IsMatchExt(ext string) bool {
for _, myExt := range pi.Extensions() {
if ext == myExt {
return true
}
}
return false
}

func (_ PngImage) Extensions() []string {
return []string{".png", ".PNG"}
}
28 changes: 28 additions & 0 deletions kadai1/hokita/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package main

import (
"fmt"
"imgconv/imgconv"
"os"
)

const (
ExitCodeOk int = iota
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iotaは値に意味がなく、区別が付けばいいときのみに使います。
そのため、0が成功、それ以外が失敗となっている終了コードに使わない方が良いでしょう。

ExitCodeError
)

func main() {
os.Exit(run())
}

func run() int {
err := imgconv.Call()

if err != nil {
fmt.Fprintln(os.Stderr, err)
return ExitCodeError
}

fmt.Println("Conversion finished!")
return ExitCodeOk
}
Binary file added kadai1/hokita/testdata/dir1/gopher2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added kadai1/hokita/testdata/gopher1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.