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

Kadai2 akuchii #28

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 8 additions & 0 deletions kadai2/akuchii/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# io.Readerとio.Writerについて調べてみよう

## 標準パッケージでどのように使われているか
os.File, os.Stdin, os.Stdout, bytes.buffersなどデータの読み書きを行う処理の部分に使われている

## io.Readerとio.Writerがあることでどういう利点があるのか具体例を挙げて考えてみる
例えばデータを読み書きする処理があったときに、io.Reader, io.Writerがあることで使う側は内部実装を知らなくても使うことができる。
内部実装を知らずに使えるので、読み書き先をDBからjson, csvなどに切り替えたい時も容易に行うことができる。
159 changes: 159 additions & 0 deletions kadai2/akuchii/converter/converter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Package converter converts image extension to target extension
package converter

import (
"fmt"
"image"
"image/gif"
"image/jpeg"
"image/png"
"io"
"os"
"path/filepath"
)

var supportedExts = map[string]bool{
"png": true,
"jpeg": true,
"jpg": true,
"gif": true,
}

// Encoder encodes image
type Encoder interface {
Encode(io.Writer, image.Image) error
}

// Converter converts image to other format of afterExt
type Converter struct {
path string
outDir string
afterExt string
encoder Encoder
}

// JpegEncoder encodes image to jpeg format
type JpegEncoder struct {
}

// PngEncoder encodes image to png format
type PngEncoder struct {
}

// GifEncoder encodes image to gif format
type GifEncoder struct {
}

// Encode encodes to jpeg
func (c *JpegEncoder) Encode(w io.Writer, img image.Image) error {
return jpeg.Encode(w, img, nil)
}

// Encode encodes to png
func (c *PngEncoder) Encode(w io.Writer, img image.Image) error {
return png.Encode(w, img)
}

// Encode encodes to gif
func (c *GifEncoder) Encode(w io.Writer, img image.Image) error {
return gif.Encode(w, img, nil)
}

// Encode encodes to specific format
func (c *Converter) Encode(w io.Writer, img image.Image) error {
return c.encoder.Encode(w, img)
}

// Decode decodes input image
func (c *Converter) Decode(r io.Reader) (image.Image, error) {
img, _, err := image.Decode(r)
return img, err
}

// NewConverter creates new converter instance
func NewConverter(path, outDir, afterExt string) (*Converter, error) {
var encoder Encoder
switch afterExt {
case "jpg", "jpeg":
encoder = &JpegEncoder{}
case "png":
encoder = &PngEncoder{}
case "gif":
encoder = &GifEncoder{}
}
return &Converter{path, outDir, afterExt, encoder}, nil
}

// Execute reads file from path, convert file extension to afterExt and save it in outDir
func (c *Converter) Execute() error {
if _, ok := supportedExts[c.afterExt]; !ok {
return fmt.Errorf("%s is not supported ext", c.afterExt)
}

in, err := os.Open(c.path)
if err != nil {
return err
}

defer in.Close()

img, err := c.Decode(in)
if err != nil {
return err
}

err = c.createDstDir()
if err != nil {
return err
}

dstPath, err := c.getDstPath()
if err != nil {
return err
}

out, err := os.Create(dstPath)
if err != nil {
return err
}
defer out.Close()

err = c.Encode(out, img)
if err != nil {
return err
}
return nil
}

func (c *Converter) getDstDir() (string, error) {
srcAbs, err := filepath.Abs(c.path)
if err != nil {
return "", err
}
return filepath.Join(filepath.Dir(srcAbs), c.outDir), nil
}

func (c *Converter) createDstDir() error {
dstDir, err := c.getDstDir()
if err != nil {
return err
}

if _, err := os.Stat(dstDir); os.IsNotExist(err) {
err = os.MkdirAll(dstDir, os.ModePerm)
if err != nil {
return err
}
}
return nil
}

func (c *Converter) getDstPath() (string, error) {
filenameWithoutExt := filepath.Base(c.path[:len(c.path)-len(filepath.Ext(c.path))])
dstDir, err := c.getDstDir()
if err != nil {
return "", err
}
dstPath := filepath.Join(dstDir, filenameWithoutExt) + "." + c.afterExt
return dstPath, nil
}
72 changes: 72 additions & 0 deletions kadai2/akuchii/converter/converter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package converter_test
Copy link

Choose a reason for hiding this comment

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

このパッケージ名にした理由が気になります。

Copy link
Author

Choose a reason for hiding this comment

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

https://tech.mercari.com/entry/2018/08/08/080000
の記事を参考にして、converterではなくconverter_testにしました。
converterと別パッケージにすることでパッケージを使う人と同じ状況(exportされたものしかアクセスされない)でテストを書くことができるのが良い、という理解です。

見直してたらpackage名と変数名が被ってたので変数名変えましたa8e7884


import (
"os"
"path/filepath"
"reflect"
"testing"

"github.com/gopherdojo/dojo4/kadai2/akuchii/converter"
)

func TestConvert_NewConverter(t *testing.T) {
cases := []struct {
afterExt string
expectedEncoder reflect.Type
}{
{"png", reflect.ValueOf(&converter.PngEncoder{}).Type()},
{"jpeg", reflect.ValueOf(&converter.JpegEncoder{}).Type()},
{"gif", reflect.ValueOf(&converter.GifEncoder{}).Type()},
}
for _, c := range cases {
conv, err := converter.NewConverter("", "", c.afterExt)
if err != nil {
t.Fatalf("failed to crete new converter:%s", err)
}
if reflect.ValueOf(conv.ExportEncoder()).Type() != c.expectedEncoder {
t.Fatalf("invalid encoder")
}
}
}

func TestConverter_Execute(t *testing.T) {
cases := []struct {
target string
outDir string
afterExt string
hasError bool
}{
{"test.png", "out", "jpeg", false},
{"test.jpeg", "out", "gif", false},
{"test.gif", "out", "png", false},
{"test.webp", "out", "png", true},
{"test.png", "out", "webp", true},
{"not_exist_file.png", "out", "jpeg", true},
}

for _, c := range cases {
srcPath := filepath.Join("..", "testdata", c.target)
filenameWithoutExt := testGetFileNameWithoutExt(t, c.target)
dstPath := filepath.Join("..", "testdata", c.outDir, filenameWithoutExt) + "." + c.afterExt
conv, err := converter.NewConverter(srcPath, c.outDir, c.afterExt)
if err != nil {
t.Fatalf("fail to create converter: %s", err)
}
err = conv.Execute()
if err != nil && !c.hasError {
t.Fatalf("fail to convert image: %s", err)
}
if _, err := os.Stat(dstPath); os.IsNotExist(err) && !c.hasError {
t.Fatalf("fail to generate converted image: %s", err)
}
if !c.hasError {
os.Remove(dstPath)
}
}
}

func testGetFileNameWithoutExt(t *testing.T, target string) string {
t.Helper()

return target[:len(target)-len(filepath.Ext(target))]
}
5 changes: 5 additions & 0 deletions kadai2/akuchii/converter/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package converter

func (c *Converter) ExportEncoder() Encoder {
return c.encoder
}
88 changes: 88 additions & 0 deletions kadai2/akuchii/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package main

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

"github.com/gopherdojo/dojo4/kadai2/akuchii/converter"
)

// extension before convert
var beforeExt string

// extension after convert
var afterExt string

// source directory
var srcDir string

// destination directory
var dstDir string

// exit code
const (
ExitCodeOK int = iota // 0
ExitCodeError // 1
)

func init() {
flag.StringVar(&beforeExt, "b", "jpeg", "extension before convert")
flag.StringVar(&afterExt, "a", "png", "extension before convert")
flag.StringVar(&srcDir, "s", "", "source directory to convert files")
flag.StringVar(&dstDir, "d", "out", "destination directory to convert files")
}

func main() {
flag.Parse()

err := validateArgs()
if err != nil {
fmt.Println(err)
os.Exit(ExitCodeError)
}

fmt.Printf("start to convert ext before: %s, after: %s\n", beforeExt, afterExt)
err = filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
if filepath.Ext(path) == "."+beforeExt {
conv, err := converter.NewConverter(path, dstDir, afterExt)
if err != nil {
fmt.Println(err)
os.Exit(ExitCodeError)
}

err = conv.Execute()
if err != nil {
fmt.Println(err)
os.Exit(ExitCodeError)
}
fmt.Printf("%s is converted to %s\n", path, afterExt)
}
return nil
})
if err != nil {
fmt.Println(err)
os.Exit(ExitCodeError)
}
fmt.Println("convert is finished")
os.Exit(ExitCodeOK)
}

// validateArgs validates input arguments
func validateArgs() error {
if beforeExt == "" || afterExt == "" || srcDir == "" || dstDir == "" {
flag.PrintDefaults()
return errors.New("empty arg is not allowed")
}

if _, err := os.Stat(srcDir); os.IsNotExist(err) {
Copy link

@knsh14 knsh14 Nov 18, 2018

Choose a reason for hiding this comment

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

これだと画像ファイルは存在するけど読込できないパターン(例えば壊れてるとかパーミッションが無いとか)に対応できなさそう?

Copy link
Author

Choose a reason for hiding this comment

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

確認してみます!

Copy link
Author

Choose a reason for hiding this comment

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

srcDirが権限のないパーミッションだとos.IsNotExist(err)がtrueにならず、srcDir以下に画像ファイルあってもfilepath.Walkで検出されませんでした...

指定されたsrcDirが読み込めない場合はerrorを出してあげたほうがよさそうですね。

return err
}

if beforeExt == afterExt {
return fmt.Errorf("before and after ext is same: %s", beforeExt)
}
return nil
}
Empty file.
Binary file added kadai2/akuchii/testdata/test.gif
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 kadai2/akuchii/testdata/test.jpeg
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 kadai2/akuchii/testdata/test.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 kadai2/akuchii/testdata/test.webp
Binary file not shown.