From 04cd96a64e4968a80d517de46e64a25b3f5621f7 Mon Sep 17 00:00:00 2001 From: Hidetake Iwata Date: Tue, 26 Jun 2018 20:42:12 +0900 Subject: [PATCH 1/3] Initial import --- kadai2/int128/.gitignore | 1 + kadai2/int128/LICENSE | 13 ++ kadai2/int128/Makefile | 13 ++ kadai2/int128/README.md | 87 ++++++++++ kadai2/int128/images/conversion.go | 46 +++++ kadai2/int128/images/conversion_test.go | 56 ++++++ kadai2/int128/images/format.go | 73 ++++++++ kadai2/int128/images/images.go | 2 + kadai2/int128/main.go | 49 ++++++ kadai2/int128/options/options.go | 95 +++++++++++ kadai2/int128/options/options_test.go | 216 ++++++++++++++++++++++++ 11 files changed, 651 insertions(+) create mode 100644 kadai2/int128/.gitignore create mode 100644 kadai2/int128/LICENSE create mode 100644 kadai2/int128/Makefile create mode 100644 kadai2/int128/README.md create mode 100644 kadai2/int128/images/conversion.go create mode 100644 kadai2/int128/images/conversion_test.go create mode 100644 kadai2/int128/images/format.go create mode 100644 kadai2/int128/images/images.go create mode 100644 kadai2/int128/main.go create mode 100644 kadai2/int128/options/options.go create mode 100644 kadai2/int128/options/options_test.go diff --git a/kadai2/int128/.gitignore b/kadai2/int128/.gitignore new file mode 100644 index 0000000..3384c4a --- /dev/null +++ b/kadai2/int128/.gitignore @@ -0,0 +1 @@ +/kadai2 diff --git a/kadai2/int128/LICENSE b/kadai2/int128/LICENSE new file mode 100644 index 0000000..274acab --- /dev/null +++ b/kadai2/int128/LICENSE @@ -0,0 +1,13 @@ + Copyright 2018 Hidetake Iwata + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/kadai2/int128/Makefile b/kadai2/int128/Makefile new file mode 100644 index 0000000..a4df9c6 --- /dev/null +++ b/kadai2/int128/Makefile @@ -0,0 +1,13 @@ +.PHONY: all test doc + +all: kadai2 + +kadai2: *.go */*.go + go build -o kadai2 + +test: *.go */*.go + go test -v ./... + +doc: *.go */*.go + godoc -ex cmd/github.com/gopherdojo/dojo2/kadai2/int128/images + godoc -ex cmd/github.com/gopherdojo/dojo2/kadai2/int128/options diff --git a/kadai2/int128/README.md b/kadai2/int128/README.md new file mode 100644 index 0000000..cf7a8ae --- /dev/null +++ b/kadai2/int128/README.md @@ -0,0 +1,87 @@ +# kadai1 + +`kadai1` is a command to convert image files. + + +## Getting Started + +``` +Usage: kadai1 FILE or DIRECTORY... + -from string + Source image format: auto, jpg, png, gif (default "jpg") + -gif-colors int + GIF number of colors (default 256) + -jpeg-quality int + JPEG quality (default 75) + -png-compression string + PNG compression level: default, no, best-speed, best-compression (default "default") + -to string + Destination image format: jpg, png, gif (default "png") +``` + +The command skips any invalid format files, for example: + +``` +$ kadai1 main.go photo.jpg +2018/06/19 10:36:41 main.go -> main.png +2018/06/19 10:36:41 Skipped main.go: Error while decoding file main.go: invalid JPEG format: missing SOI marker +2018/06/19 10:36:41 photo.jpg -> photo.png +``` + + +### Examples + +To convert files in the folder from JPEG to PNG: + +```sh +kadai1 my-photos/ +``` + +To convert files from PNG to 16-colors GIF: + +```sh +kadai1 -from png -to gif -gif-colors 16 my-photo.png +``` + +To reduce size of the JPEG file: + +```sh +kadai1 -to jpg -jpeg-quality 25 +``` + + +### Development + +Build: + +```sh +make +``` + +Test: + +```sh +make test +``` + +Show GoDoc: + +```sh +make doc +``` + + +## 課題 + +> 次の仕様を満たすコマンドを作って下さい +> - ディレクトリを指定する +> - 指定したディレクトリ以下のJPGファイルをPNGに変換 +> - ディレクトリ以下は再帰的に処理する +> - 変換前と変換後の画像形式を指定できる +> +> 以下を満たすように開発してください +> - mainパッケージと分離する +> - 自作パッケージと標準パッケージと準標準パッケージのみ使う +> - 準標準パッケージ:golang.org/x以下のパッケージ +> - ユーザ定義型を作ってみる +> - GoDocを生成してみる diff --git a/kadai2/int128/images/conversion.go b/kadai2/int128/images/conversion.go new file mode 100644 index 0000000..3f65d7a --- /dev/null +++ b/kadai2/int128/images/conversion.go @@ -0,0 +1,46 @@ +package images + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// Conversion represents an image conversion between given formats. +type Conversion struct { + Decoder Decoder + Encoder Encoder + DestinationExt string +} + +// ReplaceExt returns filename replaced the extension with DestinationExt. +// For example, if `DestinationExt` is `png`, `ReplaceExt("hello.jpg")` will return `"hello.png"`. +func (c *Conversion) ReplaceExt(filename string) string { + tail := filepath.Ext(filename) + head := strings.TrimSuffix(filename, tail) + return fmt.Sprintf("%s.%s", head, c.DestinationExt) +} + +// Do converts the source file to destination. +// `source` and `destination` must be file path. +func (c *Conversion) Do(source string, destination string) error { + r, err := os.Open(source) + if err != nil { + return fmt.Errorf("Error while opening source file %s: %s", source, err) + } + defer r.Close() + m, err := c.Decoder.Decode(r) + if err != nil { + return fmt.Errorf("Error while decoding file %s: %s", source, err) + } + w, err := os.Create(destination) + if err != nil { + return fmt.Errorf("Error while opening destination file %s: %s", destination, err) + } + defer w.Close() + if err := c.Encoder.Encode(w, m); err != nil { + return fmt.Errorf("Error while encoding to file %s: %s", destination, err) + } + return nil +} diff --git a/kadai2/int128/images/conversion_test.go b/kadai2/int128/images/conversion_test.go new file mode 100644 index 0000000..f659304 --- /dev/null +++ b/kadai2/int128/images/conversion_test.go @@ -0,0 +1,56 @@ +package images + +import ( + "fmt" + "image" + "image/jpeg" + "image/png" + "io/ioutil" + "os" +) + +func ExampleConversion_ReplaceExt() { + conversion := &Conversion{DestinationExt: "png"} + destination := conversion.ReplaceExt("hello.jpg") + fmt.Println(destination) + // Output: hello.png +} + +func ExampleConversion_Do() { + // Create a JPEG image and plot a pixel + jpegImage := image.NewRGBA(image.Rect(0, 0, 100, 200)) + jpegFile, err := ioutil.TempFile("", "jpeg") + if err != nil { + panic(err) + } + defer jpegFile.Close() + defer os.Remove(jpegFile.Name()) + if err := jpeg.Encode(jpegFile, jpegImage, nil); err != nil { + panic(err) + } + + // Convert from JPEG to PNG + conversion := &Conversion{ + Decoder: &JPEG{}, + Encoder: &PNG{}, + } + source := jpegFile.Name() + destination := conversion.ReplaceExt(source) + if err := conversion.Do(source, destination); err != nil { + panic(err) + } + + // Read the PNG image + pngFile, err := os.Open(destination) + if err != nil { + panic(err) + } + defer pngFile.Close() + defer os.Remove(pngFile.Name()) + pngImage, err := png.Decode(pngFile) + if err != nil { + panic(err) + } + fmt.Printf("size=%+v", pngImage.Bounds().Size()) + // Output: size=(100,200) +} diff --git a/kadai2/int128/images/format.go b/kadai2/int128/images/format.go new file mode 100644 index 0000000..b87deef --- /dev/null +++ b/kadai2/int128/images/format.go @@ -0,0 +1,73 @@ +package images + +import ( + "image" + "image/gif" + "image/jpeg" + "image/png" + "io" +) + +// Decoder transforms binary to an image. +type Decoder interface { + Decode(io.Reader) (image.Image, error) +} + +// Encoder transforms an image to binary. +type Encoder interface { + Encode(io.Writer, image.Image) error +} + +// AutoDetect represents auto-detect. +type AutoDetect struct{} + +// Decode automatically detects format and decodes the binary. +func (f *AutoDetect) Decode(r io.Reader) (image.Image, error) { + m, _, err := image.Decode(r) + return m, err +} + +// JPEG represents JPEG format. +type JPEG struct { + Options jpeg.Options +} + +// Decode transforms the JPEG binary to image. +func (f *JPEG) Decode(r io.Reader) (image.Image, error) { + return jpeg.Decode(r) +} + +// Encode transforms the JPEG image to binary. +func (f *JPEG) Encode(w io.Writer, m image.Image) error { + return jpeg.Encode(w, m, &f.Options) +} + +// PNG represents PNG format. +type PNG struct { + Options png.Encoder +} + +// Decode transforms the PNG binary to image. +func (f *PNG) Decode(r io.Reader) (image.Image, error) { + return png.Decode(r) +} + +// Encode transforms the PNG image to binary. +func (f *PNG) Encode(w io.Writer, m image.Image) error { + return f.Options.Encode(w, m) +} + +// GIF represents GIF format. +type GIF struct { + Options gif.Options +} + +// Decode transforms the GIF binary to image. +func (f *GIF) Decode(r io.Reader) (image.Image, error) { + return gif.Decode(r) +} + +// Encode transforms the GIF image to binary. +func (f *GIF) Encode(w io.Writer, m image.Image) error { + return gif.Encode(w, m, &f.Options) +} diff --git a/kadai2/int128/images/images.go b/kadai2/int128/images/images.go new file mode 100644 index 0000000..24a2139 --- /dev/null +++ b/kadai2/int128/images/images.go @@ -0,0 +1,2 @@ +// Package images provides image conversion between various formats. +package images diff --git a/kadai2/int128/main.go b/kadai2/int128/main.go new file mode 100644 index 0000000..bdcd8fe --- /dev/null +++ b/kadai2/int128/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "log" + "os" + "path/filepath" + + "github.com/gopherdojo/dojo2/kadai2/int128/images" + "github.com/gopherdojo/dojo2/kadai2/int128/options" +) + +func main() { + opts, err := options.Parse(os.Args) + if err != nil { + os.Exit(1) + } + decoder, err := opts.Decoder() + if err != nil { + log.Fatalf("Error: %s", err) + } + encoder, err := opts.Encoder() + if err != nil { + log.Fatalf("Error: %s", err) + } + conversion := &images.Conversion{ + Decoder: decoder, + Encoder: encoder, + DestinationExt: *opts.To, + } + for _, parent := range opts.Paths { + if err := filepath.Walk(parent, func(path string, info os.FileInfo, err error) error { + switch { + case err != nil: + return err + case !info.IsDir(): + destination := conversion.ReplaceExt(path) + log.Printf("%s -> %s", path, destination) + if err := conversion.Do(path, destination); err != nil { + log.Printf("Skipped %s: %s", path, err) + } + return nil + default: + return nil + } + }); err != nil { + log.Printf("Skipped %s: %s", parent, err) + } + } +} diff --git a/kadai2/int128/options/options.go b/kadai2/int128/options/options.go new file mode 100644 index 0000000..f6e75b8 --- /dev/null +++ b/kadai2/int128/options/options.go @@ -0,0 +1,95 @@ +// Package options provides parsing command line options. +package options + +import ( + "flag" + "fmt" + "image/gif" + "image/jpeg" + "image/png" + + "github.com/gopherdojo/dojo2/kadai2/int128/images" +) + +// Options represents command line options. +type Options struct { + Paths []string + From *string + To *string + JPEGQuality *int + PNGCompression *string + GIFColors *int +} + +// Parse returns the command line arguments which includes the command name. +// No argument or any unknown flag will show the usage and return an error. +// Caller should exit on an error. +func Parse(args []string) (*Options, error) { + f := flag.NewFlagSet(args[0], flag.ContinueOnError) + opts := &Options{ + From: f.String("from", "jpg", "Source image format: auto, jpg, png, gif"), + To: f.String("to", "png", "Destination image format: jpg, png, gif"), + JPEGQuality: f.Int("jpeg-quality", jpeg.DefaultQuality, "JPEG quality"), + PNGCompression: f.String("png-compression", "default", "PNG compression level: default, no, best-speed, best-compression"), + GIFColors: f.Int("gif-colors", 256, "GIF number of colors"), + } + f.Usage = func() { + fmt.Fprintf(f.Output(), "Usage: %s FILE or DIRECTORY...\n", f.Name()) + f.PrintDefaults() + } + if err := f.Parse(args[1:]); err != nil { + return nil, err + } + if f.NArg() == 0 { + f.Usage() + return nil, fmt.Errorf("too few argument") + } + opts.Paths = f.Args() + return opts, nil +} + +// Decoder returns a decoder configured with the options. +func (opts *Options) Decoder() (images.Decoder, error) { + switch *opts.From { + case "auto": + return &images.AutoDetect{}, nil + case "jpg": + return &images.JPEG{}, nil + case "png": + return &images.PNG{}, nil + case "gif": + return &images.GIF{}, nil + } + return nil, fmt.Errorf("Unknown source image format: %s", *opts.From) +} + +// Encoder returns a encoder configured with the options. +func (opts *Options) Encoder() (images.Encoder, error) { + switch *opts.To { + case "jpg": + return &images.JPEG{Options: jpeg.Options{Quality: *opts.JPEGQuality}}, nil + case "png": + c, err := opts.pngCompression() + if err != nil { + return nil, err + } + return &images.PNG{Options: png.Encoder{CompressionLevel: c}}, nil + case "gif": + return &images.GIF{Options: gif.Options{NumColors: *opts.GIFColors}}, nil + } + return nil, fmt.Errorf("Unknown destination image format: %s", *opts.To) +} + +func (opts *Options) pngCompression() (png.CompressionLevel, error) { + switch *opts.PNGCompression { + case "default": + return png.DefaultCompression, nil + case "no": + return png.NoCompression, nil + case "best-speed": + return png.BestSpeed, nil + case "best-compression": + return png.BestCompression, nil + } + return png.DefaultCompression, fmt.Errorf("Unknown PNG compression level: %s", *opts.PNGCompression) +} diff --git a/kadai2/int128/options/options_test.go b/kadai2/int128/options/options_test.go new file mode 100644 index 0000000..e387611 --- /dev/null +++ b/kadai2/int128/options/options_test.go @@ -0,0 +1,216 @@ +package options + +import ( + "image/jpeg" + "image/png" + "strings" + "testing" + + "github.com/gopherdojo/dojo2/kadai2/int128/images" +) + +const arg0 = "kadai2" + +func TestNoArg(t *testing.T) { + _, err := Parse([]string{arg0}) + if err == nil { + t.Errorf("err wants non-nil but %v", err) + } +} + +func TestUnknownFlag(t *testing.T) { + _, err := Parse([]string{arg0, "-foo"}) + if err == nil { + t.Errorf("err wants non-nil but %v", err) + } +} + +func TestDefaultArgs(t *testing.T) { + opts, err := Parse([]string{arg0, "foo.jpg"}) + if err != nil { + t.Fatal(err) + } + decoder, err := opts.Decoder() + if err != nil { + t.Fatal(err) + } + if _, ok := decoder.(*images.JPEG); !ok { + t.Errorf("decoder wants JPEG but %+v", decoder) + } + encoder, err := opts.Encoder() + if err != nil { + t.Fatal(err) + } + if _, ok := encoder.(*images.PNG); !ok { + t.Errorf("encoder wants PNG but %+v", encoder) + } +} + +func TestInvalidSourceFormat(t *testing.T) { + opts, err := Parse([]string{arg0, "-from", "bar", "foo.jpg"}) + if err != nil { + t.Fatal(err) + } + if _, err := opts.Encoder(); err != nil { + t.Fatal(err) + } + if _, err := opts.Decoder(); err == nil { + t.Errorf("err wants non-nil but nil") + } else if !strings.Contains(err.Error(), "bar") { + t.Errorf("error message wants bar but %s", err.Error()) + } +} + +func TestInvalidDestinationFormat(t *testing.T) { + opts, err := Parse([]string{arg0, "-to", "bar", "foo.jpg"}) + if err != nil { + t.Fatal(err) + } + if _, err := opts.Decoder(); err != nil { + t.Fatal(err) + } + if _, err := opts.Encoder(); err == nil { + t.Errorf("err wants non-nil but nil") + } else if !strings.Contains(err.Error(), "bar") { + t.Errorf("error message wants bar but %s", err.Error()) + } +} + +func TestFromPNGToJPEG(t *testing.T) { + for _, m := range []struct { + Args []string + Quality int + }{ + {[]string{arg0, "-from", "png", "-to", "jpg", "foo.jpg"}, jpeg.DefaultQuality}, + {[]string{arg0, "-from", "png", "-to", "jpg", "-jpeg-quality", "5", "foo.jpg"}, 5}, + } { + opts, err := Parse(m.Args) + if err != nil { + t.Fatal(err) + } + decoder, err := opts.Decoder() + if err != nil { + t.Fatal(err) + } + if _, ok := decoder.(*images.PNG); !ok { + t.Errorf("decoder wants PNG but %+v", decoder) + } + encoder, err := opts.Encoder() + if err != nil { + t.Fatal(err) + } + if e, ok := encoder.(*images.JPEG); !ok { + t.Errorf("encoder wants JPEG but %+v", encoder) + } else if e.Options.Quality != m.Quality { + t.Errorf("NumColors wants %d but %d", m.Quality, e.Options.Quality) + } + } +} + +func TestFromJPEGToGIF(t *testing.T) { + for _, m := range []struct { + Args []string + NumColors int + }{ + {[]string{arg0, "-to", "gif", "foo.jpg"}, 256}, + {[]string{arg0, "-to", "gif", "-gif-colors", "5", "foo.jpg"}, 5}, + } { + opts, err := Parse(m.Args) + if err != nil { + t.Fatal(err) + } + decoder, err := opts.Decoder() + if err != nil { + t.Fatal(err) + } + if _, ok := decoder.(*images.JPEG); !ok { + t.Errorf("decoder wants JPEG but %+v", decoder) + } + encoder, err := opts.Encoder() + if err != nil { + t.Fatal(err) + } + if e, ok := encoder.(*images.GIF); !ok { + t.Errorf("encoder wants GIF but %+v", encoder) + } else if e.Options.NumColors != m.NumColors { + t.Errorf("NumColors wants %d but %d", m.NumColors, e.Options.NumColors) + } + } +} + +func TestFromGIFToPNG(t *testing.T) { + for _, m := range []struct { + Args []string + CompressionLevel png.CompressionLevel + }{ + {[]string{arg0, "-from", "gif", "foo.jpg"}, png.DefaultCompression}, + {[]string{arg0, "-from", "gif", "-to", "png", "foo.jpg"}, png.DefaultCompression}, + {[]string{arg0, "-from", "gif", "-to", "png", "-png-compression", "no", "foo.jpg"}, png.NoCompression}, + {[]string{arg0, "-from", "gif", "-to", "png", "-png-compression", "best-speed", "foo.jpg"}, png.BestSpeed}, + {[]string{arg0, "-from", "gif", "-to", "png", "-png-compression", "best-compression", "foo.jpg"}, png.BestCompression}, + } { + opts, err := Parse(m.Args) + if err != nil { + t.Fatal(err) + } + decoder, err := opts.Decoder() + if err != nil { + t.Fatal(err) + } + if _, ok := decoder.(*images.GIF); !ok { + t.Errorf("decoder wants GIF but %+v", decoder) + } + encoder, err := opts.Encoder() + if err != nil { + t.Fatal(err) + } + if e, ok := encoder.(*images.PNG); !ok { + t.Errorf("encoder wants PNG but %+v", encoder) + } else if e.Options.CompressionLevel != m.CompressionLevel { + t.Errorf("NumColors wants %v but %v", m.CompressionLevel, e.Options.CompressionLevel) + } + } +} + +func TestInvalidPNGCompressionLevel(t *testing.T) { + opts, err := Parse([]string{arg0, "-to", "png", "-png-compression", "zzz", "foo.jpg"}) + if err != nil { + t.Fatal(err) + } + if _, err := opts.Decoder(); err != nil { + t.Fatal(err) + } + if _, err := opts.Encoder(); err == nil { + t.Errorf("err wants non-nil but nil") + } else if !strings.Contains(err.Error(), "zzz") { + t.Errorf("error message wants zzz but %s", err.Error()) + } +} + +func TestFromAutoToPNG(t *testing.T) { + for _, m := range []struct { + Args []string + CompressionLevel png.CompressionLevel + }{ + {[]string{arg0, "-from", "auto", "foo.jpg"}, png.DefaultCompression}, + } { + opts, err := Parse(m.Args) + if err != nil { + t.Fatal(err) + } + decoder, err := opts.Decoder() + if err != nil { + t.Fatal(err) + } + if _, ok := decoder.(*images.AutoDetect); !ok { + t.Errorf("decoder wants AutoDetect but %+v", decoder) + } + encoder, err := opts.Encoder() + if err != nil { + t.Fatal(err) + } + if _, ok := encoder.(*images.PNG); !ok { + t.Errorf("encoder wants PNG but %+v", encoder) + } + } +} From 8b96ed088ec20b7d2302568e9d555c7b32a39572 Mon Sep 17 00:00:00 2001 From: Hidetake Iwata Date: Wed, 27 Jun 2018 17:08:46 +0900 Subject: [PATCH 2/3] Add kadai2 --- kadai2/int128/Makefile | 16 ++- kadai2/int128/README.md | 193 ++++++++++++++++++++++++++----------- kadai2/int128/main_test.go | 102 ++++++++++++++++++++ 3 files changed, 254 insertions(+), 57 deletions(-) create mode 100644 kadai2/int128/main_test.go diff --git a/kadai2/int128/Makefile b/kadai2/int128/Makefile index a4df9c6..05c1843 100644 --- a/kadai2/int128/Makefile +++ b/kadai2/int128/Makefile @@ -1,4 +1,4 @@ -.PHONY: all test doc +.PHONY: all test doc showReaderWriter all: kadai2 @@ -6,8 +6,20 @@ kadai2: *.go */*.go go build -o kadai2 test: *.go */*.go - go test -v ./... + go test -v -cover ./... doc: *.go */*.go godoc -ex cmd/github.com/gopherdojo/dojo2/kadai2/int128/images godoc -ex cmd/github.com/gopherdojo/dojo2/kadai2/int128/options + +showReaderWriterImplements: + cd `go env GOROOT`/src && \ + find . -name '*.go' -and -not -name '*_test.go' -and -not -path '*/internal/*' -and -not -path '*/vendor/*' | \ + xargs egrep -R 'func \(\w+ \*?[A-Z]\w+\) (Read|Write)\(\w+ \[\]byte\)' | \ + column -t -s: + +showReaderWriterRefs: + cd `go env GOROOT`/src && \ + find . -name '*.go' -and -not -name '*_test.go' -and -not -path '*/internal/*' -and -not -path '*/vendor/*' | \ + xargs egrep -R 'func .* io.(Reader|Writer)' | \ + column -t -s: diff --git a/kadai2/int128/README.md b/kadai2/int128/README.md index cf7a8ae..0881061 100644 --- a/kadai2/int128/README.md +++ b/kadai2/int128/README.md @@ -1,87 +1,170 @@ -# kadai1 +# kadai2 -`kadai1` is a command to convert image files. +See also https://github.com/gopherdojo/dojo2/tree/kadai1-int128/kadai1/int128. -## Getting Started +## `io.Reader` と `io.Writer` -``` -Usage: kadai1 FILE or DIRECTORY... - -from string - Source image format: auto, jpg, png, gif (default "jpg") - -gif-colors int - GIF number of colors (default 256) - -jpeg-quality int - JPEG quality (default 75) - -png-compression string - PNG compression level: default, no, best-speed, best-compression (default "default") - -to string - Destination image format: jpg, png, gif (default "png") -``` +`io.Reader` と `io.Writer` はストリームの読み書きを行うためのインタフェースで、Javaにおける `InputStream` や `OutputStream` に相当する。 + +### 標準パッケージにおける利用 + +Go 1.10では `io.Reader` と `io.Writer` は以下のように定義されている。 -The command skips any invalid format files, for example: +```go +package io +type Reader interface { + Read(p []byte) (n int, err error) +} + +type Writer interface { + Write(p []byte) (n int, err error) +} ``` -$ kadai1 main.go photo.jpg -2018/06/19 10:36:41 main.go -> main.png -2018/06/19 10:36:41 Skipped main.go: Error while decoding file main.go: invalid JPEG format: missing SOI marker -2018/06/19 10:36:41 photo.jpg -> photo.png + +Go 1.10の標準パッケージでは16個の構造体が `Read([]byte)` メソッドを実装している。 +また、15個の構造体が `Write([]byte)` メソッドを実装している。 +(テストコードおよび `internal` パッケージを除く) + +具体的には以下のメソッドが存在する。 + +```go +% make showReaderWriterImplements +./bufio/bufio.go func (b *Reader) Read(p []byte) (n int, err error) { +./bufio/bufio.go func (b *Writer) Write(p []byte) (nn int, err error) { +./crypto/cipher/io.go func (r StreamReader) Read(dst []byte) (n int, err error) { +./crypto/cipher/io.go func (w StreamWriter) Write(src []byte) (n int, err error) { +./crypto/tls/conn.go func (c *Conn) Write(b []byte) (int, error) { +./crypto/tls/conn.go func (c *Conn) Read(b []byte) (n int, err error) { +./compress/flate/deflate.go func (w *Writer) Write(data []byte) (n int, err error) { +./compress/gzip/gzip.go func (z *Writer) Write(p []byte) (int, error) { +./compress/gzip/gunzip.go func (z *Reader) Read(p []byte) (n int, err error) { +./compress/zlib/writer.go func (z *Writer) Write(p []byte) (n int, err error) { +./strings/reader.go func (r *Reader) Read(b []byte) (n int, err error) { +./strings/builder.go func (b *Builder) Write(p []byte) (int, error) { +./net/net.go func (v *Buffers) Read(p []byte) (n int, err error) { +./net/http/httptest/recorder.go func (rw *ResponseRecorder) Write(buf []byte) (int, error) { +./archive/tar/writer.go func (tw *Writer) Write(b []byte) (int, error) { +./archive/tar/reader.go func (tr *Reader) Read(b []byte) (int, error) { +./bytes/buffer.go func (b *Buffer) Write(p []byte) (n int, err error) { +./bytes/buffer.go func (b *Buffer) Read(p []byte) (n int, err error) { +./bytes/reader.go func (r *Reader) Read(b []byte) (n int, err error) { +./io/io.go func (l *LimitedReader) Read(p []byte) (n int, err error) { +./io/io.go func (s *SectionReader) Read(p []byte) (n int, err error) { +./io/pipe.go func (r *PipeReader) Read(data []byte) (n int, err error) { +./io/pipe.go func (w *PipeWriter) Write(data []byte) (n int, err error) { +./math/rand/rand.go func (r *Rand) Read(p []byte) (n int, err error) { +./log/syslog/syslog.go func (w *Writer) Write(b []byte) (int, error) { +./mime/multipart/multipart.go func (p *Part) Read(d []byte) (n int, err error) { +./mime/quotedprintable/writer.go func (w *Writer) Write(p []byte) (n int, err error) { +./mime/quotedprintable/reader.go func (r *Reader) Read(p []byte) (n int, err error) { +./os/file.go func (f *File) Read(b []byte) (n int, err error) { +./os/file.go func (f *File) Write(b []byte) (n int, err error) { +./text/tabwriter/tabwriter.go func (b *Writer) Write(buf []byte) (n int, err error) { ``` +メソッドの役割をまとめるとおおよそ以下のようになる。 -### Examples +- ファイルの読み書き +- ネットワーク通信 +- 暗号化、復号 +- ファイルの圧縮、展開(ZIP/TAR) +- MIMEエンコード、デコード +- バイト配列や文字列の処理 +- 行指向やトークン分割の処理 -To convert files in the folder from JPEG to PNG: +このように、Goの標準パッケージでは入出力に関わるインタフェースが抽象化されていることが分かる。 -```sh -kadai1 my-photos/ -``` +### 抽象化の利点 + +入出力に関わるインタフェースを抽象化することで、コードをシンプルに保ちながら拡張性を持たせることができる。 -To convert files from PNG to 16-colors GIF: +例えば、 `image/jpeg` パッケージでは以下のメソッドが定義されている。 -```sh -kadai1 -from png -to gif -gif-colors 16 my-photo.png +```go +func Decode(r io.Reader) (image.Image, error) {} ``` -To reduce size of the JPEG file: +ローカルにあるJPEGファイルを読み込みたい場合は `os.Open()` の戻り値を渡せばよい。 + +```go +func Example_io_Reader_File() { + f, err := os.Open("image.jpg") + if err != nil { + panic(err) + } + defer f.Close() + img, err := jpeg.Decode(f) + if err != nil { + panic(err) + } + fmt.Printf("size=%+v", img.Bounds()) + // Output: size=(0,0)-(1000,750) +} +``` -```sh -kadai1 -to jpg -jpeg-quality 25 +また、リモートにあるJPEGファイルを読み込みたい場合は `http.Get()` の戻り値を渡せばよい。 + +```go +func Example_io_Reader_HTTP() { + resp, err := http.Get("https://upload.wikimedia.org/wikipedia/commons/b/b2/JPEG_compression_Example.jpg") + if err != nil { + panic(err) + } + defer resp.Body.Close() + img, err := jpeg.Decode(resp.Body) + if err != nil { + panic(err) + } + fmt.Printf("size=%+v", img.Bounds()) + // Output: size=(0,0)-(1000,750) +} ``` +もちろん、独自に定義した型を渡すこともできる。 -### Development +```go +type DummyReader struct{} -Build: +func (r *DummyReader) Read(p []byte) (int, error) { + return 0, io.EOF +} -```sh -make +func Example_io_Reader_DummyReader() { + jpeg.Decode(&DummyReader{}) +} ``` -Test: +このように、インタフェースによる抽象化を行うことで、JPEGデータがローカルにある場合でもリモートにある場合でも同じメソッドを使うことができる。 + +もし、インタフェースが使えない場合は、以下のように具象型ごとに関数を定義することになる。 -```sh -make test +```go +func DecodeFile(f *os.File) (image.Image, error) {} +func DecodeHTTPResponseBody(r /* レスポンスボディ型 */) (image.Image, error) {} +func DecodeZIPFile(f *zip.File) (image.Image, error) {} ``` -Show GoDoc: +これでは具象型が増えるたびに関数を定義する必要があり、冗長なコードが増えてしまう。 +また、標準パッケージの外側で独自に定義した型を受け取ることができない問題がある。 -```sh -make doc -``` +## kadai1のリファクタリングとテスト + +前回の課題1でほとんどのテストコードを書いていたため、課題2では `main_test.go` を追加しました。 -## 課題 -> 次の仕様を満たすコマンドを作って下さい -> - ディレクトリを指定する -> - 指定したディレクトリ以下のJPGファイルをPNGに変換 -> - ディレクトリ以下は再帰的に処理する -> - 変換前と変換後の画像形式を指定できる +## 課題2 + +> io.Readerとio.Writerについて調べてみよう +> +> - 標準パッケージでどのように使われているか +> - io.Readerとio.Writerがあることでどういう利点があるのか具体例を挙げて考えてみる +> +> 1回目の宿題のテストを作ってみて下さい > -> 以下を満たすように開発してください -> - mainパッケージと分離する -> - 自作パッケージと標準パッケージと準標準パッケージのみ使う -> - 準標準パッケージ:golang.org/x以下のパッケージ -> - ユーザ定義型を作ってみる -> - GoDocを生成してみる +> - テストのしやすさを考えてリファクタリングしてみる +> - テストのカバレッジを取ってみる +> - テーブル駆動テストを行う +> - テストヘルパーを作ってみる diff --git a/kadai2/int128/main_test.go b/kadai2/int128/main_test.go new file mode 100644 index 0000000..d635cd1 --- /dev/null +++ b/kadai2/int128/main_test.go @@ -0,0 +1,102 @@ +package main + +import ( + "fmt" + "image" + "image/jpeg" + "image/png" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "testing" +) + +// TestMain performs the integration test for JPEG-PNG conversion. +func TestMain(t *testing.T) { + dir, err := ioutil.TempDir("", "main") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + subdir := filepath.Join(dir, "subdir") + if err := os.Mkdir(subdir, 0755); err != nil { + t.Fatal(err) + } + + // Create test fixtures + if err := createJPEG(filepath.Join(dir, "image1.jpg"), 100, 200); err != nil { + t.Fatal(err) + } + if err := createJPEG(filepath.Join(subdir, "image2.jpg"), 300, 400); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(filepath.Join(subdir, "dummy.txt"), []byte("dummy"), 0644); err != nil { + t.Fatal(err) + } + + // Run main + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + os.Args = []string{"main", dir} + main() + + // Assert that destination contains PNG files + if err := assertFilesIn(dir, []string{"image1.jpg", "image1.png"}); err != nil { + t.Error(err) + } + if err := assertFilesIn(subdir, []string{"dummy.txt", "image2.jpg", "image2.png"}); err != nil { + t.Error(err) + } + + // Assert that PNG files are valid + if err := assertPNG(filepath.Join(dir, "image1.png"), 100, 200); err != nil { + t.Error(err) + } + if err := assertPNG(filepath.Join(subdir, "image2.png"), 300, 400); err != nil { + t.Error(err) + } +} + +func assertFilesIn(dir string, expectedFiles []string) error { + children, err := ioutil.ReadDir(dir) + if err != nil { + return err + } + files := make([]string, 0) + for _, child := range children { + if !child.IsDir() { + files = append(files, child.Name()) + } + } + if !reflect.DeepEqual(expectedFiles, files) { + return fmt.Errorf("Directory %s wants %v but %v", dir, expectedFiles, files) + } + return nil +} + +func createJPEG(name string, width int, height int) error { + r, err := os.Create(name) + if err != nil { + return err + } + defer r.Close() + img := image.NewRGBA(image.Rect(0, 0, width, height)) + return jpeg.Encode(r, img, nil) +} + +func assertPNG(name string, width int, height int) error { + r, err := os.Open(name) + if err != nil { + return err + } + defer r.Close() + c, err := png.DecodeConfig(r) + if err != nil { + return err + } + if c.Width != width || c.Height != height { + return fmt.Errorf("PNG %s wants %dx%d but %dx%d", name, width, height, c.Width, c.Height) + } + return nil +} From 11b7c56161109bdf1c90c358cec719960d23ccd6 Mon Sep 17 00:00:00 2001 From: Hidetake Iwata Date: Thu, 28 Jun 2018 10:29:58 +0900 Subject: [PATCH 3/3] Refactor with test helper --- kadai2/int128/main_test.go | 52 +++++++++++++++----------------------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/kadai2/int128/main_test.go b/kadai2/int128/main_test.go index d635cd1..49edc50 100644 --- a/kadai2/int128/main_test.go +++ b/kadai2/int128/main_test.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "image" "image/jpeg" "image/png" @@ -25,12 +24,8 @@ func TestMain(t *testing.T) { } // Create test fixtures - if err := createJPEG(filepath.Join(dir, "image1.jpg"), 100, 200); err != nil { - t.Fatal(err) - } - if err := createJPEG(filepath.Join(subdir, "image2.jpg"), 300, 400); err != nil { - t.Fatal(err) - } + createJPEG(t, filepath.Join(dir, "image1.jpg"), 100, 200) + createJPEG(t, filepath.Join(subdir, "image2.jpg"), 300, 400) if err := ioutil.WriteFile(filepath.Join(subdir, "dummy.txt"), []byte("dummy"), 0644); err != nil { t.Fatal(err) } @@ -42,26 +37,19 @@ func TestMain(t *testing.T) { main() // Assert that destination contains PNG files - if err := assertFilesIn(dir, []string{"image1.jpg", "image1.png"}); err != nil { - t.Error(err) - } - if err := assertFilesIn(subdir, []string{"dummy.txt", "image2.jpg", "image2.png"}); err != nil { - t.Error(err) - } + assertFilesIn(t, dir, []string{"image1.jpg", "image1.png"}) + assertFilesIn(t, subdir, []string{"dummy.txt", "image2.jpg", "image2.png"}) // Assert that PNG files are valid - if err := assertPNG(filepath.Join(dir, "image1.png"), 100, 200); err != nil { - t.Error(err) - } - if err := assertPNG(filepath.Join(subdir, "image2.png"), 300, 400); err != nil { - t.Error(err) - } + assertPNG(t, filepath.Join(dir, "image1.png"), 100, 200) + assertPNG(t, filepath.Join(subdir, "image2.png"), 300, 400) } -func assertFilesIn(dir string, expectedFiles []string) error { +func assertFilesIn(t *testing.T, dir string, expectedFiles []string) { + t.Helper() children, err := ioutil.ReadDir(dir) if err != nil { - return err + t.Fatal(err) } files := make([]string, 0) for _, child := range children { @@ -70,33 +58,35 @@ func assertFilesIn(dir string, expectedFiles []string) error { } } if !reflect.DeepEqual(expectedFiles, files) { - return fmt.Errorf("Directory %s wants %v but %v", dir, expectedFiles, files) + t.Errorf("Directory %s wants %v but %v", dir, expectedFiles, files) } - return nil } -func createJPEG(name string, width int, height int) error { +func createJPEG(t *testing.T, name string, width int, height int) { + t.Helper() r, err := os.Create(name) if err != nil { - return err + t.Fatal(err) } defer r.Close() img := image.NewRGBA(image.Rect(0, 0, width, height)) - return jpeg.Encode(r, img, nil) + if err := jpeg.Encode(r, img, nil); err != nil { + t.Fatal(err) + } } -func assertPNG(name string, width int, height int) error { +func assertPNG(t *testing.T, name string, width int, height int) { + t.Helper() r, err := os.Open(name) if err != nil { - return err + t.Fatal(err) } defer r.Close() c, err := png.DecodeConfig(r) if err != nil { - return err + t.Fatal(err) } if c.Width != width || c.Height != height { - return fmt.Errorf("PNG %s wants %dx%d but %dx%d", name, width, height, c.Width, c.Height) + t.Errorf("PNG %s wants %dx%d but %dx%d", name, width, height, c.Width, c.Height) } - return nil }