From 9ebc5be39c5bad22ca0f97849d1ad475bacdc950 Mon Sep 17 00:00:00 2001 From: Andrew Bonventre Date: Mon, 15 Jul 2013 10:57:01 +1000 Subject: [PATCH] image/gif: add writer implementation R=r, nigeltao CC=golang-dev https://golang.org/cl/10896043 --- src/pkg/go/build/deps_test.go | 2 +- src/pkg/image/gif/reader.go | 2 +- src/pkg/image/gif/writer.go | 329 ++++++++++++++++++++++ src/pkg/image/gif/writer_test.go | 204 ++++++++++++++ src/pkg/image/testdata/video-005.gray.gif | Bin 0 -> 14505 bytes 5 files changed, 535 insertions(+), 2 deletions(-) create mode 100644 src/pkg/image/gif/writer.go create mode 100644 src/pkg/image/gif/writer_test.go create mode 100644 src/pkg/image/testdata/video-005.gray.gif diff --git a/src/pkg/go/build/deps_test.go b/src/pkg/go/build/deps_test.go index 71b1bcf060..eb2eb515a5 100644 --- a/src/pkg/go/build/deps_test.go +++ b/src/pkg/go/build/deps_test.go @@ -202,7 +202,7 @@ var pkgDeps = map[string][]string{ "go/build": {"L4", "OS", "GOPARSER"}, "html": {"L4"}, "image/draw": {"L4"}, - "image/gif": {"L4", "compress/lzw"}, + "image/gif": {"L4", "compress/lzw", "image/draw"}, "image/jpeg": {"L4"}, "image/png": {"L4", "compress/zlib"}, "index/suffixarray": {"L4", "regexp"}, diff --git a/src/pkg/image/gif/reader.go b/src/pkg/image/gif/reader.go index 5adc8b97fa..8b0298a29f 100644 --- a/src/pkg/image/gif/reader.go +++ b/src/pkg/image/gif/reader.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package gif implements a GIF image decoder. +// Package gif implements a GIF image decoder and encoder. // // The GIF specification is at http://www.w3.org/Graphics/GIF/spec-gif89a.txt. package gif diff --git a/src/pkg/image/gif/writer.go b/src/pkg/image/gif/writer.go new file mode 100644 index 0000000000..23f8b1b3ad --- /dev/null +++ b/src/pkg/image/gif/writer.go @@ -0,0 +1,329 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gif + +import ( + "bufio" + "compress/lzw" + "errors" + "image" + "image/color" + "image/draw" + "io" +) + +// Graphic control extension fields. +const ( + gcLabel = 0xF9 + gcBlockSize = 0x04 +) + +var log2Lookup = [8]int{2, 4, 8, 16, 32, 64, 128, 256} + +func log2(x int) int { + for i, v := range log2Lookup { + if x <= v { + return i + } + } + return -1 +} + +// Little-endian. +func writeUint16(b []uint8, u uint16) { + b[0] = uint8(u) + b[1] = uint8(u >> 8) +} + +// writer is a buffered writer. +type writer interface { + Flush() error + io.Writer + io.ByteWriter +} + +// encoder encodes an image to the GIF format. +type encoder struct { + // w is the writer to write to. err is the first error encountered during + // writing. All attempted writes after the first error become no-ops. + w writer + err error + // g is a reference to the data that is being encoded. + g *GIF + // bitsPerPixel is the number of bits required to represent each color + // in the image. + bitsPerPixel int + // buf is a scratch buffer. It must be at least 768 so we can write the color map. + buf [1024]byte +} + +// blockWriter writes the block structure of GIF image data, which +// comprises (n, (n bytes)) blocks, with 1 <= n <= 255. It is the +// writer given to the LZW encoder, which is thus immune to the +// blocking. +type blockWriter struct { + e *encoder +} + +func (b blockWriter) Write(data []byte) (int, error) { + if b.e.err != nil { + return 0, b.e.err + } + if len(data) == 0 { + return 0, nil + } + total := 0 + for total < len(data) { + n := copy(b.e.buf[1:256], data[total:]) + total += n + b.e.buf[0] = uint8(n) + + n, b.e.err = b.e.w.Write(b.e.buf[:n+1]) + if b.e.err != nil { + return 0, b.e.err + } + } + return total, b.e.err +} + +func (e *encoder) flush() { + if e.err != nil { + return + } + e.err = e.w.Flush() +} + +func (e *encoder) write(p []byte) { + if e.err != nil { + return + } + _, e.err = e.w.Write(p) +} + +func (e *encoder) writeByte(b byte) { + if e.err != nil { + return + } + e.err = e.w.WriteByte(b) +} + +func (e *encoder) writeHeader() { + if e.err != nil { + return + } + _, e.err = io.WriteString(e.w, "GIF89a") + if e.err != nil { + return + } + + // TODO: This bases the global color table on the first image + // only. + pm := e.g.Image[0] + // Logical screen width and height. + writeUint16(e.buf[0:2], uint16(pm.Bounds().Dx())) + writeUint16(e.buf[2:4], uint16(pm.Bounds().Dy())) + e.write(e.buf[:4]) + + e.bitsPerPixel = log2(len(pm.Palette)) + 1 + e.buf[0] = 0x80 | ((uint8(e.bitsPerPixel) - 1) << 4) | (uint8(e.bitsPerPixel) - 1) + e.buf[1] = 0x00 // Background Color Index. + e.buf[2] = 0x00 // Pixel Aspect Ratio. + e.write(e.buf[:3]) + + // Global Color Table. + e.writeColorTable(pm.Palette, e.bitsPerPixel-1) + + // Add animation info if necessary. + if len(e.g.Image) > 1 { + e.buf[0] = 0x21 // Extension Introducer. + e.buf[1] = 0xff // Application Label. + e.buf[2] = 0x0b // Block Size. + e.write(e.buf[:3]) + _, e.err = io.WriteString(e.w, "NETSCAPE2.0") // Application Identifier. + if e.err != nil { + return + } + e.buf[0] = 0x03 // Block Size. + e.buf[1] = 0x01 // Sub-block Index. + writeUint16(e.buf[2:4], uint16(e.g.LoopCount)) + e.buf[4] = 0x00 // Block Terminator. + e.write(e.buf[:5]) + } +} + +func (e *encoder) writeColorTable(p color.Palette, size int) { + if e.err != nil { + return + } + + for i := 0; i < log2Lookup[size]; i++ { + if i < len(p) { + r, g, b, _ := p[i].RGBA() + e.buf[3*i+0] = uint8(r >> 8) + e.buf[3*i+1] = uint8(g >> 8) + e.buf[3*i+2] = uint8(b >> 8) + } else { + // Pad with black. + e.buf[3*i+0] = 0x00 + e.buf[3*i+1] = 0x00 + e.buf[3*i+2] = 0x00 + } + } + e.write(e.buf[:3*log2Lookup[size]]) +} + +func (e *encoder) writeImageBlock(pm *image.Paletted, delay int) { + if e.err != nil { + return + } + + if len(pm.Palette) == 0 { + e.err = errors.New("gif: cannot encode image block with empty palette") + return + } + + b := pm.Bounds() + if b.Dx() >= 1<<16 || b.Dy() >= 1<<16 || b.Min.X < 0 || b.Min.X >= 1<<16 || b.Min.Y < 0 || b.Min.Y >= 1<<16 { + e.err = errors.New("gif: image block is too large to encode") + return + } + + transparentIndex := -1 + for i, c := range pm.Palette { + if _, _, _, a := c.RGBA(); a == 0 { + transparentIndex = i + break + } + } + + if delay > 0 || transparentIndex != -1 { + e.buf[0] = sExtension // Extension Introducer. + e.buf[1] = gcLabel // Graphic Control Label. + e.buf[2] = gcBlockSize // Block Size. + if transparentIndex != -1 { + e.buf[3] = 0x01 + } else { + e.buf[3] = 0x00 + } + writeUint16(e.buf[4:6], uint16(delay)) // Delay Time (1/100ths of a second) + + // Transparent color index. + if transparentIndex != -1 { + e.buf[6] = uint8(transparentIndex) + } else { + e.buf[6] = 0x00 + } + e.buf[7] = 0x00 // Block Terminator. + e.write(e.buf[:8]) + } + e.buf[0] = sImageDescriptor + writeUint16(e.buf[1:3], uint16(b.Min.X)) + writeUint16(e.buf[3:5], uint16(b.Min.Y)) + writeUint16(e.buf[5:7], uint16(b.Dx())) + writeUint16(e.buf[7:9], uint16(b.Dy())) + e.write(e.buf[:9]) + + paddedSize := log2(len(pm.Palette)) // Size of Local Color Table: 2^(1+n). + // Interlacing is not supported. + e.writeByte(0x80 | uint8(paddedSize)) + + // Local Color Table. + e.writeColorTable(pm.Palette, paddedSize) + + litWidth := e.bitsPerPixel + if litWidth < 2 { + litWidth = 2 + } + e.writeByte(uint8(litWidth)) // LZW Minimum Code Size. + + lzww := lzw.NewWriter(blockWriter{e: e}, lzw.LSB, litWidth) + _, e.err = lzww.Write(pm.Pix) + if e.err != nil { + lzww.Close() + return + } + lzww.Close() + e.writeByte(0x00) // Block Terminator. +} + +// Options are the encoding parameters. +type Options struct { + // NumColors is the maximum number of colors used in the image. + // It ranges from 1 to 256. + NumColors int + + // Quantizer is used to produce a palette with size NumColors. + // color.Plan9Palette is used in place of a nil Quantizer. + Quantizer draw.Quantizer + + // Drawer is used to convert the source image to the desired palette. + // draw.FloydSteinberg is used in place of a nil Drawer. + Drawer draw.Drawer +} + +// EncodeAll writes the images in g to w in GIF format with the +// given loop count and delay between frames. +func EncodeAll(w io.Writer, g *GIF) error { + if len(g.Image) == 0 { + return errors.New("gif: must provide at least one image") + } + + if len(g.Image) != len(g.Delay) { + return errors.New("gif: mismatched image and delay lengths") + } + if g.LoopCount < 0 { + g.LoopCount = 0 + } + + e := encoder{g: g} + if ww, ok := w.(writer); ok { + e.w = ww + } else { + e.w = bufio.NewWriter(w) + } + + e.writeHeader() + for i, pm := range g.Image { + e.writeImageBlock(pm, g.Delay[i]) + } + e.writeByte(sTrailer) + e.flush() + return e.err +} + +// Encode writes the Image m to w in GIF format. +func Encode(w io.Writer, m image.Image, o *Options) error { + // Check for bounds and size restrictions. + b := m.Bounds() + if b.Dx() >= 1<<16 || b.Dy() >= 1<<16 { + return errors.New("gif: image is too large to encode") + } + + opts := Options{} + if o != nil { + opts = *o + } + if opts.NumColors < 1 || 256 < opts.NumColors { + opts.NumColors = 256 + } + if opts.Drawer == nil { + opts.Drawer = draw.FloydSteinberg + } + + pm, ok := m.(*image.Paletted) + if !ok || len(pm.Palette) > opts.NumColors { + // TODO: Pick a better sub-sample of the Plan 9 palette. + pm = image.NewPaletted(b, color.Plan9Palette[:opts.NumColors]) + if opts.Quantizer != nil { + pm.Palette = opts.Quantizer.Quantize(make(color.Palette, 0, opts.NumColors), m) + } + opts.Drawer.Draw(pm, b, m, image.ZP) + } + + return EncodeAll(w, &GIF{ + Image: []*image.Paletted{pm}, + Delay: []int{0}, + }) +} diff --git a/src/pkg/image/gif/writer_test.go b/src/pkg/image/gif/writer_test.go new file mode 100644 index 0000000000..c1ada769c2 --- /dev/null +++ b/src/pkg/image/gif/writer_test.go @@ -0,0 +1,204 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gif + +import ( + "bytes" + "image" + "image/color" + _ "image/png" + "io/ioutil" + "math/rand" + "os" + "testing" +) + +func readImg(filename string) (image.Image, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + m, _, err := image.Decode(f) + return m, err +} + +func readGIF(filename string) (*GIF, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + return DecodeAll(f) +} + +func delta(u0, u1 uint32) int64 { + d := int64(u0) - int64(u1) + if d < 0 { + return -d + } + return d +} + +// averageDelta returns the average delta in RGB space. The two images must +// have the same bounds. +func averageDelta(m0, m1 image.Image) int64 { + b := m0.Bounds() + var sum, n int64 + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + c0 := m0.At(x, y) + c1 := m1.At(x, y) + r0, g0, b0, _ := c0.RGBA() + r1, g1, b1, _ := c1.RGBA() + sum += delta(r0, r1) + sum += delta(g0, g1) + sum += delta(b0, b1) + n += 3 + } + } + return sum / n +} + +var testCase = []struct { + filename string + tolerance int64 +}{ + {"../testdata/video-001.png", 1 << 12}, + {"../testdata/video-001.gif", 0}, + {"../testdata/video-001.interlaced.gif", 0}, +} + +func TestWriter(t *testing.T) { + for _, tc := range testCase { + m0, err := readImg(tc.filename) + if err != nil { + t.Error(tc.filename, err) + continue + } + var buf bytes.Buffer + err = Encode(&buf, m0, nil) + if err != nil { + t.Error(tc.filename, err) + continue + } + m1, err := Decode(&buf) + if err != nil { + t.Error(tc.filename, err) + continue + } + if m0.Bounds() != m1.Bounds() { + t.Errorf("%s, bounds differ: %v and %v", tc.filename, m0.Bounds(), m1.Bounds()) + continue + } + // Compare the average delta to the tolerance level. + avgDelta := averageDelta(m0, m1) + if avgDelta > tc.tolerance { + t.Errorf("%s: average delta is too high. expected: %d, got %d", tc.filename, tc.tolerance, avgDelta) + continue + } + } +} + +var frames = []string{ + "../testdata/video-001.gif", + "../testdata/video-005.gray.gif", +} + +func TestEncodeAll(t *testing.T) { + g0 := &GIF{ + Image: make([]*image.Paletted, len(frames)), + Delay: make([]int, len(frames)), + LoopCount: 5, + } + for i, f := range frames { + m, err := readGIF(f) + if err != nil { + t.Error(f, err) + } + g0.Image[i] = m.Image[0] + } + var buf bytes.Buffer + if err := EncodeAll(&buf, g0); err != nil { + t.Fatal("EncodeAll:", err) + } + g1, err := DecodeAll(&buf) + if err != nil { + t.Fatal("DecodeAll:", err) + } + if g0.LoopCount != g1.LoopCount { + t.Errorf("loop counts differ: %d and %d", g0.LoopCount, g1.LoopCount) + } + for i := range g0.Image { + m0, m1 := g0.Image[i], g1.Image[i] + if m0.Bounds() != m1.Bounds() { + t.Errorf("%s, bounds differ: %v and %v", frames[i], m0.Bounds(), m1.Bounds()) + } + d0, d1 := g0.Delay[i], g1.Delay[i] + if d0 != d1 { + t.Errorf("%s: delay values differ: %d and %d", frames[i], d0, d1) + } + } + + g1.Delay = make([]int, 1) + if err := EncodeAll(ioutil.Discard, g1); err == nil { + t.Error("expected error from mismatched delay and image slice lengths") + } + if err := EncodeAll(ioutil.Discard, &GIF{}); err == nil { + t.Error("expected error from providing empty gif") + } +} + +func BenchmarkEncode(b *testing.B) { + b.StopTimer() + + bo := image.Rect(0, 0, 640, 480) + rnd := rand.New(rand.NewSource(123)) + + // Restrict to a 256-color paletted image to avoid quantization path. + palette := make(color.Palette, 256) + for i := range palette { + palette[i] = color.RGBA{ + uint8(rnd.Intn(256)), + uint8(rnd.Intn(256)), + uint8(rnd.Intn(256)), + 255, + } + } + img := image.NewPaletted(image.Rect(0, 0, 640, 480), palette) + for y := bo.Min.Y; y < bo.Max.Y; y++ { + for x := bo.Min.X; x < bo.Max.X; x++ { + img.Set(x, y, palette[rnd.Intn(256)]) + } + } + + b.SetBytes(640 * 480 * 4) + b.StartTimer() + for i := 0; i < b.N; i++ { + Encode(ioutil.Discard, img, nil) + } +} + +func BenchmarkQuantizedEncode(b *testing.B) { + b.StopTimer() + img := image.NewRGBA(image.Rect(0, 0, 640, 480)) + bo := img.Bounds() + rnd := rand.New(rand.NewSource(123)) + for y := bo.Min.Y; y < bo.Max.Y; y++ { + for x := bo.Min.X; x < bo.Max.X; x++ { + img.SetRGBA(x, y, color.RGBA{ + uint8(rnd.Intn(256)), + uint8(rnd.Intn(256)), + uint8(rnd.Intn(256)), + 255, + }) + } + } + b.SetBytes(640 * 480 * 4) + b.StartTimer() + for i := 0; i < b.N; i++ { + Encode(ioutil.Discard, img, nil) + } +} diff --git a/src/pkg/image/testdata/video-005.gray.gif b/src/pkg/image/testdata/video-005.gray.gif new file mode 100644 index 0000000000000000000000000000000000000000..23350d6dc127b6ddc775b3a80c19e134088488ba GIT binary patch literal 14505 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 VQl_Dm+=nW!VO$l@MU((c{Xg$+Nx%RA literal 0 HcmV?d00001 -- 2.48.1