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..05c1843 --- /dev/null +++ b/kadai2/int128/Makefile @@ -0,0 +1,25 @@ +.PHONY: all test doc showReaderWriter + +all: kadai2 + +kadai2: *.go */*.go + go build -o kadai2 + +test: *.go */*.go + 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 new file mode 100644 index 0000000..0881061 --- /dev/null +++ b/kadai2/int128/README.md @@ -0,0 +1,170 @@ +# kadai2 + +See also https://github.com/gopherdojo/dojo2/tree/kadai1-int128/kadai1/int128. + + +## `io.Reader` と `io.Writer` + +`io.Reader` と `io.Writer` はストリームの読み書きを行うためのインタフェースで、Javaにおける `InputStream` や `OutputStream` に相当する。 + +### 標準パッケージにおける利用 + +Go 1.10では `io.Reader` と `io.Writer` は以下のように定義されている。 + +```go +package io + +type Reader interface { + Read(p []byte) (n int, err error) +} + +type Writer interface { + Write(p []byte) (n int, err error) +} +``` + +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) { +``` + +メソッドの役割をまとめるとおおよそ以下のようになる。 + +- ファイルの読み書き +- ネットワーク通信 +- 暗号化、復号 +- ファイルの圧縮、展開(ZIP/TAR) +- MIMEエンコード、デコード +- バイト配列や文字列の処理 +- 行指向やトークン分割の処理 + +このように、Goの標準パッケージでは入出力に関わるインタフェースが抽象化されていることが分かる。 + +### 抽象化の利点 + +入出力に関わるインタフェースを抽象化することで、コードをシンプルに保ちながら拡張性を持たせることができる。 + +例えば、 `image/jpeg` パッケージでは以下のメソッドが定義されている。 + +```go +func Decode(r io.Reader) (image.Image, error) {} +``` + +ローカルにある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) +} +``` + +また、リモートにある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) +} +``` + +もちろん、独自に定義した型を渡すこともできる。 + +```go +type DummyReader struct{} + +func (r *DummyReader) Read(p []byte) (int, error) { + return 0, io.EOF +} + +func Example_io_Reader_DummyReader() { + jpeg.Decode(&DummyReader{}) +} +``` + +このように、インタフェースによる抽象化を行うことで、JPEGデータがローカルにある場合でもリモートにある場合でも同じメソッドを使うことができる。 + +もし、インタフェースが使えない場合は、以下のように具象型ごとに関数を定義することになる。 + +```go +func DecodeFile(f *os.File) (image.Image, error) {} +func DecodeHTTPResponseBody(r /* レスポンスボディ型 */) (image.Image, error) {} +func DecodeZIPFile(f *zip.File) (image.Image, error) {} +``` + +これでは具象型が増えるたびに関数を定義する必要があり、冗長なコードが増えてしまう。 +また、標準パッケージの外側で独自に定義した型を受け取ることができない問題がある。 + + +## kadai1のリファクタリングとテスト + +前回の課題1でほとんどのテストコードを書いていたため、課題2では `main_test.go` を追加しました。 + + +## 課題2 + +> io.Readerとio.Writerについて調べてみよう +> +> - 標準パッケージでどのように使われているか +> - io.Readerとio.Writerがあることでどういう利点があるのか具体例を挙げて考えてみる +> +> 1回目の宿題のテストを作ってみて下さい +> +> - テストのしやすさを考えてリファクタリングしてみる +> - テストのカバレッジを取ってみる +> - テーブル駆動テストを行う +> - テストヘルパーを作ってみる 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/main_test.go b/kadai2/int128/main_test.go new file mode 100644 index 0000000..49edc50 --- /dev/null +++ b/kadai2/int128/main_test.go @@ -0,0 +1,92 @@ +package main + +import ( + "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 + 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) + } + + // Run main + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + os.Args = []string{"main", dir} + main() + + // Assert that destination contains PNG files + assertFilesIn(t, dir, []string{"image1.jpg", "image1.png"}) + assertFilesIn(t, subdir, []string{"dummy.txt", "image2.jpg", "image2.png"}) + + // Assert that PNG files are valid + assertPNG(t, filepath.Join(dir, "image1.png"), 100, 200) + assertPNG(t, filepath.Join(subdir, "image2.png"), 300, 400) +} + +func assertFilesIn(t *testing.T, dir string, expectedFiles []string) { + t.Helper() + children, err := ioutil.ReadDir(dir) + if err != nil { + t.Fatal(err) + } + files := make([]string, 0) + for _, child := range children { + if !child.IsDir() { + files = append(files, child.Name()) + } + } + if !reflect.DeepEqual(expectedFiles, files) { + t.Errorf("Directory %s wants %v but %v", dir, expectedFiles, files) + } +} + +func createJPEG(t *testing.T, name string, width int, height int) { + t.Helper() + r, err := os.Create(name) + if err != nil { + t.Fatal(err) + } + defer r.Close() + img := image.NewRGBA(image.Rect(0, 0, width, height)) + if err := jpeg.Encode(r, img, nil); err != nil { + t.Fatal(err) + } +} + +func assertPNG(t *testing.T, name string, width int, height int) { + t.Helper() + r, err := os.Open(name) + if err != nil { + t.Fatal(err) + } + defer r.Close() + c, err := png.DecodeConfig(r) + if err != nil { + t.Fatal(err) + } + if c.Width != width || c.Height != height { + t.Errorf("PNG %s wants %dx%d but %dx%d", name, width, height, c.Width, c.Height) + } +} 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) + } + } +}