diff options
Diffstat (limited to 'src/image/gif')
-rw-r--r-- | src/image/gif/reader.go | 465 | ||||
-rw-r--r-- | src/image/gif/reader_test.go | 247 | ||||
-rw-r--r-- | src/image/gif/writer.go | 333 | ||||
-rw-r--r-- | src/image/gif/writer_test.go | 227 |
4 files changed, 1272 insertions, 0 deletions
diff --git a/src/image/gif/reader.go b/src/image/gif/reader.go new file mode 100644 index 000000000..5a863e204 --- /dev/null +++ b/src/image/gif/reader.go @@ -0,0 +1,465 @@ +// Copyright 2011 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 implements a GIF image decoder and encoder. +// +// The GIF specification is at http://www.w3.org/Graphics/GIF/spec-gif89a.txt. +package gif + +import ( + "bufio" + "compress/lzw" + "errors" + "fmt" + "image" + "image/color" + "io" +) + +var ( + errNotEnough = errors.New("gif: not enough image data") + errTooMuch = errors.New("gif: too much image data") + errBadPixel = errors.New("gif: invalid pixel value") +) + +// If the io.Reader does not also have ReadByte, then decode will introduce its own buffering. +type reader interface { + io.Reader + io.ByteReader +} + +// Masks etc. +const ( + // Fields. + fColorMapFollows = 1 << 7 + + // Image fields. + ifLocalColorTable = 1 << 7 + ifInterlace = 1 << 6 + ifPixelSizeMask = 7 + + // Graphic control flags. + gcTransparentColorSet = 1 << 0 +) + +// Section indicators. +const ( + sExtension = 0x21 + sImageDescriptor = 0x2C + sTrailer = 0x3B +) + +// Extensions. +const ( + eText = 0x01 // Plain Text + eGraphicControl = 0xF9 // Graphic Control + eComment = 0xFE // Comment + eApplication = 0xFF // Application +) + +// decoder is the type used to decode a GIF file. +type decoder struct { + r reader + + // From header. + vers string + width int + height int + flags byte + headerFields byte + backgroundIndex byte + loopCount int + delayTime int + + // Unused from header. + aspect byte + + // From image descriptor. + imageFields byte + + // From graphics control. + transparentIndex byte + hasTransparentIndex bool + + // Computed. + pixelSize uint + globalColorMap color.Palette + + // Used when decoding. + delay []int + image []*image.Paletted + tmp [1024]byte // must be at least 768 so we can read color map +} + +// blockReader parses the block structure of GIF image data, which +// comprises (n, (n bytes)) blocks, with 1 <= n <= 255. It is the +// reader given to the LZW decoder, which is thus immune to the +// blocking. After the LZW decoder completes, there will be a 0-byte +// block remaining (0, ()), which is consumed when checking that the +// blockReader is exhausted. +type blockReader struct { + r reader + slice []byte + err error + tmp [256]byte +} + +func (b *blockReader) Read(p []byte) (int, error) { + if b.err != nil { + return 0, b.err + } + if len(p) == 0 { + return 0, nil + } + if len(b.slice) == 0 { + var blockLen uint8 + blockLen, b.err = b.r.ReadByte() + if b.err != nil { + return 0, b.err + } + if blockLen == 0 { + b.err = io.EOF + return 0, b.err + } + b.slice = b.tmp[0:blockLen] + if _, b.err = io.ReadFull(b.r, b.slice); b.err != nil { + return 0, b.err + } + } + n := copy(p, b.slice) + b.slice = b.slice[n:] + return n, nil +} + +// decode reads a GIF image from r and stores the result in d. +func (d *decoder) decode(r io.Reader, configOnly bool) error { + // Add buffering if r does not provide ReadByte. + if rr, ok := r.(reader); ok { + d.r = rr + } else { + d.r = bufio.NewReader(r) + } + + err := d.readHeaderAndScreenDescriptor() + if err != nil { + return err + } + if configOnly { + return nil + } + + if d.headerFields&fColorMapFollows != 0 { + if d.globalColorMap, err = d.readColorMap(); err != nil { + return err + } + } + + for { + c, err := d.r.ReadByte() + if err != nil { + return err + } + switch c { + case sExtension: + if err = d.readExtension(); err != nil { + return err + } + + case sImageDescriptor: + m, err := d.newImageFromDescriptor() + if err != nil { + return err + } + useLocalColorMap := d.imageFields&fColorMapFollows != 0 + if useLocalColorMap { + m.Palette, err = d.readColorMap() + if err != nil { + return err + } + } else { + m.Palette = d.globalColorMap + } + if d.hasTransparentIndex && int(d.transparentIndex) < len(m.Palette) { + if !useLocalColorMap { + // Clone the global color map. + m.Palette = append(color.Palette(nil), d.globalColorMap...) + } + m.Palette[d.transparentIndex] = color.RGBA{} + } + litWidth, err := d.r.ReadByte() + if err != nil { + return err + } + if litWidth < 2 || litWidth > 8 { + return fmt.Errorf("gif: pixel size in decode out of range: %d", litWidth) + } + // A wonderfully Go-like piece of magic. + br := &blockReader{r: d.r} + lzwr := lzw.NewReader(br, lzw.LSB, int(litWidth)) + defer lzwr.Close() + if _, err = io.ReadFull(lzwr, m.Pix); err != nil { + if err != io.ErrUnexpectedEOF { + return err + } + return errNotEnough + } + // Both lzwr and br should be exhausted. Reading from them + // should yield (0, io.EOF). + if n, err := lzwr.Read(d.tmp[:1]); n != 0 || err != io.EOF { + if err != nil { + return err + } + return errTooMuch + } + if n, err := br.Read(d.tmp[:1]); n != 0 || err != io.EOF { + if err != nil { + return err + } + return errTooMuch + } + + // Check that the color indexes are inside the palette. + if len(m.Palette) < 256 { + for _, pixel := range m.Pix { + if int(pixel) >= len(m.Palette) { + return errBadPixel + } + } + } + + // Undo the interlacing if necessary. + if d.imageFields&ifInterlace != 0 { + uninterlace(m) + } + + d.image = append(d.image, m) + d.delay = append(d.delay, d.delayTime) + // The GIF89a spec, Section 23 (Graphic Control Extension) says: + // "The scope of this extension is the first graphic rendering block + // to follow." We therefore reset the GCE fields to zero. + d.delayTime = 0 + d.hasTransparentIndex = false + + case sTrailer: + if len(d.image) == 0 { + return io.ErrUnexpectedEOF + } + return nil + + default: + return fmt.Errorf("gif: unknown block type: 0x%.2x", c) + } + } +} + +func (d *decoder) readHeaderAndScreenDescriptor() error { + _, err := io.ReadFull(d.r, d.tmp[0:13]) + if err != nil { + return err + } + d.vers = string(d.tmp[0:6]) + if d.vers != "GIF87a" && d.vers != "GIF89a" { + return fmt.Errorf("gif: can't recognize format %s", d.vers) + } + d.width = int(d.tmp[6]) + int(d.tmp[7])<<8 + d.height = int(d.tmp[8]) + int(d.tmp[9])<<8 + d.headerFields = d.tmp[10] + d.backgroundIndex = d.tmp[11] + d.aspect = d.tmp[12] + d.loopCount = -1 + d.pixelSize = uint(d.headerFields&7) + 1 + return nil +} + +func (d *decoder) readColorMap() (color.Palette, error) { + if d.pixelSize > 8 { + return nil, fmt.Errorf("gif: can't handle %d bits per pixel", d.pixelSize) + } + numColors := 1 << d.pixelSize + if d.imageFields&ifLocalColorTable != 0 { + numColors = 1 << ((d.imageFields & ifPixelSizeMask) + 1) + } + numValues := 3 * numColors + _, err := io.ReadFull(d.r, d.tmp[0:numValues]) + if err != nil { + return nil, fmt.Errorf("gif: short read on color map: %s", err) + } + colorMap := make(color.Palette, numColors) + j := 0 + for i := range colorMap { + colorMap[i] = color.RGBA{d.tmp[j+0], d.tmp[j+1], d.tmp[j+2], 0xFF} + j += 3 + } + return colorMap, nil +} + +func (d *decoder) readExtension() error { + extension, err := d.r.ReadByte() + if err != nil { + return err + } + size := 0 + switch extension { + case eText: + size = 13 + case eGraphicControl: + return d.readGraphicControl() + case eComment: + // nothing to do but read the data. + case eApplication: + b, err := d.r.ReadByte() + if err != nil { + return err + } + // The spec requires size be 11, but Adobe sometimes uses 10. + size = int(b) + default: + return fmt.Errorf("gif: unknown extension 0x%.2x", extension) + } + if size > 0 { + if _, err := io.ReadFull(d.r, d.tmp[0:size]); err != nil { + return err + } + } + + // Application Extension with "NETSCAPE2.0" as string and 1 in data means + // this extension defines a loop count. + if extension == eApplication && string(d.tmp[:size]) == "NETSCAPE2.0" { + n, err := d.readBlock() + if n == 0 || err != nil { + return err + } + if n == 3 && d.tmp[0] == 1 { + d.loopCount = int(d.tmp[1]) | int(d.tmp[2])<<8 + } + } + for { + n, err := d.readBlock() + if n == 0 || err != nil { + return err + } + } +} + +func (d *decoder) readGraphicControl() error { + if _, err := io.ReadFull(d.r, d.tmp[0:6]); err != nil { + return fmt.Errorf("gif: can't read graphic control: %s", err) + } + d.flags = d.tmp[1] + d.delayTime = int(d.tmp[2]) | int(d.tmp[3])<<8 + if d.flags&gcTransparentColorSet != 0 { + d.transparentIndex = d.tmp[4] + d.hasTransparentIndex = true + } + return nil +} + +func (d *decoder) newImageFromDescriptor() (*image.Paletted, error) { + if _, err := io.ReadFull(d.r, d.tmp[0:9]); err != nil { + return nil, fmt.Errorf("gif: can't read image descriptor: %s", err) + } + left := int(d.tmp[0]) + int(d.tmp[1])<<8 + top := int(d.tmp[2]) + int(d.tmp[3])<<8 + width := int(d.tmp[4]) + int(d.tmp[5])<<8 + height := int(d.tmp[6]) + int(d.tmp[7])<<8 + d.imageFields = d.tmp[8] + + // The GIF89a spec, Section 20 (Image Descriptor) says: + // "Each image must fit within the boundaries of the Logical + // Screen, as defined in the Logical Screen Descriptor." + bounds := image.Rect(left, top, left+width, top+height) + if bounds != bounds.Intersect(image.Rect(0, 0, d.width, d.height)) { + return nil, errors.New("gif: frame bounds larger than image bounds") + } + return image.NewPaletted(bounds, nil), nil +} + +func (d *decoder) readBlock() (int, error) { + n, err := d.r.ReadByte() + if n == 0 || err != nil { + return 0, err + } + return io.ReadFull(d.r, d.tmp[0:n]) +} + +// interlaceScan defines the ordering for a pass of the interlace algorithm. +type interlaceScan struct { + skip, start int +} + +// interlacing represents the set of scans in an interlaced GIF image. +var interlacing = []interlaceScan{ + {8, 0}, // Group 1 : Every 8th. row, starting with row 0. + {8, 4}, // Group 2 : Every 8th. row, starting with row 4. + {4, 2}, // Group 3 : Every 4th. row, starting with row 2. + {2, 1}, // Group 4 : Every 2nd. row, starting with row 1. +} + +// uninterlace rearranges the pixels in m to account for interlaced input. +func uninterlace(m *image.Paletted) { + var nPix []uint8 + dx := m.Bounds().Dx() + dy := m.Bounds().Dy() + nPix = make([]uint8, dx*dy) + offset := 0 // steps through the input by sequential scan lines. + for _, pass := range interlacing { + nOffset := pass.start * dx // steps through the output as defined by pass. + for y := pass.start; y < dy; y += pass.skip { + copy(nPix[nOffset:nOffset+dx], m.Pix[offset:offset+dx]) + offset += dx + nOffset += dx * pass.skip + } + } + m.Pix = nPix +} + +// Decode reads a GIF image from r and returns the first embedded +// image as an image.Image. +func Decode(r io.Reader) (image.Image, error) { + var d decoder + if err := d.decode(r, false); err != nil { + return nil, err + } + return d.image[0], nil +} + +// GIF represents the possibly multiple images stored in a GIF file. +type GIF struct { + Image []*image.Paletted // The successive images. + Delay []int // The successive delay times, one per frame, in 100ths of a second. + LoopCount int // The loop count. +} + +// DecodeAll reads a GIF image from r and returns the sequential frames +// and timing information. +func DecodeAll(r io.Reader) (*GIF, error) { + var d decoder + if err := d.decode(r, false); err != nil { + return nil, err + } + gif := &GIF{ + Image: d.image, + LoopCount: d.loopCount, + Delay: d.delay, + } + return gif, nil +} + +// DecodeConfig returns the global color model and dimensions of a GIF image +// without decoding the entire image. +func DecodeConfig(r io.Reader) (image.Config, error) { + var d decoder + if err := d.decode(r, true); err != nil { + return image.Config{}, err + } + return image.Config{ + ColorModel: d.globalColorMap, + Width: d.width, + Height: d.height, + }, nil +} + +func init() { + image.RegisterFormat("gif", "GIF8?a", Decode, DecodeConfig) +} diff --git a/src/image/gif/reader_test.go b/src/image/gif/reader_test.go new file mode 100644 index 000000000..7b6f50436 --- /dev/null +++ b/src/image/gif/reader_test.go @@ -0,0 +1,247 @@ +// 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" + "compress/lzw" + "image" + "image/color" + "reflect" + "testing" +) + +// header, palette and trailer are parts of a valid 2x1 GIF image. +const ( + headerStr = "GIF89a" + + "\x02\x00\x01\x00" + // width=2, height=1 + "\x80\x00\x00" // headerFields=(a color map of 2 pixels), backgroundIndex, aspect + paletteStr = "\x10\x20\x30\x40\x50\x60" // the color map, also known as a palette + trailerStr = "\x3b" +) + +// lzwEncode returns an LZW encoding (with 2-bit literals) of n zeroes. +func lzwEncode(n int) []byte { + b := &bytes.Buffer{} + w := lzw.NewWriter(b, lzw.LSB, 2) + w.Write(make([]byte, n)) + w.Close() + return b.Bytes() +} + +func TestDecode(t *testing.T) { + testCases := []struct { + nPix int // The number of pixels in the image data. + extra bool // Whether to write an extra block after the LZW-encoded data. + wantErr error + }{ + {0, false, errNotEnough}, + {1, false, errNotEnough}, + {2, false, nil}, + {2, true, errTooMuch}, + {3, false, errTooMuch}, + } + for _, tc := range testCases { + b := &bytes.Buffer{} + b.WriteString(headerStr) + b.WriteString(paletteStr) + // Write an image with bounds 2x1 but tc.nPix pixels. If tc.nPix != 2 + // then this should result in an invalid GIF image. First, write a + // magic 0x2c (image descriptor) byte, bounds=(0,0)-(2,1), a flags + // byte, and 2-bit LZW literals. + b.WriteString("\x2c\x00\x00\x00\x00\x02\x00\x01\x00\x00\x02") + if tc.nPix > 0 { + enc := lzwEncode(tc.nPix) + if len(enc) > 0xff { + t.Errorf("nPix=%d, extra=%t: compressed length %d is too large", tc.nPix, tc.extra, len(enc)) + continue + } + b.WriteByte(byte(len(enc))) + b.Write(enc) + } + if tc.extra { + b.WriteString("\x01\x02") // A 1-byte payload with an 0x02 byte. + } + b.WriteByte(0x00) // An empty block signifies the end of the image data. + b.WriteString(trailerStr) + + got, err := Decode(b) + if err != tc.wantErr { + t.Errorf("nPix=%d, extra=%t\ngot %v\nwant %v", tc.nPix, tc.extra, err, tc.wantErr) + } + + if tc.wantErr != nil { + continue + } + want := &image.Paletted{ + Pix: []uint8{0, 0}, + Stride: 2, + Rect: image.Rect(0, 0, 2, 1), + Palette: color.Palette{ + color.RGBA{0x10, 0x20, 0x30, 0xff}, + color.RGBA{0x40, 0x50, 0x60, 0xff}, + }, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("nPix=%d, extra=%t\ngot %v\nwant %v", tc.nPix, tc.extra, got, want) + } + } +} + +func TestTransparentIndex(t *testing.T) { + b := &bytes.Buffer{} + b.WriteString(headerStr) + b.WriteString(paletteStr) + for transparentIndex := 0; transparentIndex < 3; transparentIndex++ { + if transparentIndex < 2 { + // Write the graphic control for the transparent index. + b.WriteString("\x21\xf9\x00\x01\x00\x00") + b.WriteByte(byte(transparentIndex)) + b.WriteByte(0) + } + // Write an image with bounds 2x1, as per TestDecode. + b.WriteString("\x2c\x00\x00\x00\x00\x02\x00\x01\x00\x00\x02") + enc := lzwEncode(2) + if len(enc) > 0xff { + t.Fatalf("compressed length %d is too large", len(enc)) + } + b.WriteByte(byte(len(enc))) + b.Write(enc) + b.WriteByte(0x00) + } + b.WriteString(trailerStr) + + g, err := DecodeAll(b) + if err != nil { + t.Fatalf("DecodeAll: %v", err) + } + c0 := color.RGBA{paletteStr[0], paletteStr[1], paletteStr[2], 0xff} + c1 := color.RGBA{paletteStr[3], paletteStr[4], paletteStr[5], 0xff} + cz := color.RGBA{} + wants := []color.Palette{ + {cz, c1}, + {c0, cz}, + {c0, c1}, + } + if len(g.Image) != len(wants) { + t.Fatalf("got %d images, want %d", len(g.Image), len(wants)) + } + for i, want := range wants { + got := g.Image[i].Palette + if !reflect.DeepEqual(got, want) { + t.Errorf("palette #%d:\ngot %v\nwant %v", i, got, want) + } + } +} + +// testGIF is a simple GIF that we can modify to test different scenarios. +var testGIF = []byte{ + 'G', 'I', 'F', '8', '9', 'a', + 1, 0, 1, 0, // w=1, h=1 (6) + 128, 0, 0, // headerFields, bg, aspect (10) + 0, 0, 0, 1, 1, 1, // color map and graphics control (13) + 0x21, 0xf9, 0x04, 0x00, 0x00, 0x00, 0xff, 0x00, // (19) + // frame 1 (0,0 - 1,1) + 0x2c, + 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x01, 0x00, // (32) + 0x00, + 0x02, 0x02, 0x4c, 0x01, 0x00, // lzw pixels + // trailer + 0x3b, +} + +func try(t *testing.T, b []byte, want string) { + _, err := DecodeAll(bytes.NewReader(b)) + var got string + if err != nil { + got = err.Error() + } + if got != want { + t.Fatalf("got %v, want %v", got, want) + } +} + +func TestBounds(t *testing.T) { + // Make a local copy of testGIF. + gif := make([]byte, len(testGIF)) + copy(gif, testGIF) + // Make the bounds too big, just by one. + gif[32] = 2 + want := "gif: frame bounds larger than image bounds" + try(t, gif, want) + + // Make the bounds too small; does not trigger bounds + // check, but now there's too much data. + gif[32] = 0 + want = "gif: too much image data" + try(t, gif, want) + gif[32] = 1 + + // Make the bounds really big, expect an error. + want = "gif: frame bounds larger than image bounds" + for i := 0; i < 4; i++ { + gif[32+i] = 0xff + } + try(t, gif, want) +} + +func TestNoPalette(t *testing.T) { + b := &bytes.Buffer{} + + // Manufacture a GIF with no palette, so any pixel at all + // will be invalid. + b.WriteString(headerStr[:len(headerStr)-3]) + b.WriteString("\x00\x00\x00") // No global palette. + + // Image descriptor: 2x1, no local palette. + b.WriteString("\x2c\x00\x00\x00\x00\x02\x00\x01\x00\x00\x02") + + // Encode the pixels: neither is in range, because there is no palette. + pix := []byte{0, 128} + enc := &bytes.Buffer{} + w := lzw.NewWriter(enc, lzw.LSB, 2) + w.Write(pix) + w.Close() + b.WriteByte(byte(len(enc.Bytes()))) + b.Write(enc.Bytes()) + b.WriteByte(0x00) // An empty block signifies the end of the image data. + + b.WriteString(trailerStr) + + try(t, b.Bytes(), "gif: invalid pixel value") +} + +func TestPixelOutsidePaletteRange(t *testing.T) { + for _, pval := range []byte{0, 1, 2, 3, 255} { + b := &bytes.Buffer{} + + // Manufacture a GIF with a 2 color palette. + b.WriteString(headerStr) + b.WriteString(paletteStr) + + // Image descriptor: 2x1, no local palette. + b.WriteString("\x2c\x00\x00\x00\x00\x02\x00\x01\x00\x00\x02") + + // Encode the pixels; some pvals trigger the expected error. + pix := []byte{pval, pval} + enc := &bytes.Buffer{} + w := lzw.NewWriter(enc, lzw.LSB, 2) + w.Write(pix) + w.Close() + b.WriteByte(byte(len(enc.Bytes()))) + b.Write(enc.Bytes()) + b.WriteByte(0x00) // An empty block signifies the end of the image data. + + b.WriteString(trailerStr) + + // No error expected, unless the pixels are beyond the 2 color palette. + want := "" + if pval >= 2 { + want = "gif: invalid pixel value" + } + try(t, b.Bytes(), want) + } +} diff --git a/src/image/gif/writer.go b/src/image/gif/writer.go new file mode 100644 index 000000000..49abde704 --- /dev/null +++ b/src/image/gif/writer.go @@ -0,0 +1,333 @@ +// 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/color/palette" + "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 + // 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 + } + + 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]) + + // All frames have a local color table, so a global color table + // is not needed. + e.buf[0] = 0x00 + e.buf[1] = 0x00 // Background Color Index. + e.buf[2] = 0x00 // Pixel Aspect Ratio. + e.write(e.buf[:3]) + + // 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 := paddedSize + 1 + if litWidth < 2 { + litWidth = 2 + } + e.writeByte(uint8(litWidth)) // LZW Minimum Code Size. + + lzww := lzw.NewWriter(blockWriter{e: e}, lzw.LSB, litWidth) + if dx := b.Dx(); dx == pm.Stride { + _, e.err = lzww.Write(pm.Pix) + if e.err != nil { + lzww.Close() + return + } + } else { + for i, y := 0, b.Min.Y; y < b.Max.Y; i, y = i+pm.Stride, y+1 { + _, e.err = lzww.Write(pm.Pix[i : i+dx]) + 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. + // palette.Plan9 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, palette.Plan9[: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/image/gif/writer_test.go b/src/image/gif/writer_test.go new file mode 100644 index 000000000..93306ffdb --- /dev/null +++ b/src/image/gif/writer_test.go @@ -0,0 +1,227 @@ +// 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 + } + } +} + +func TestSubImage(t *testing.T) { + m0, err := readImg("../testdata/video-001.gif") + if err != nil { + t.Fatalf("readImg: %v", err) + } + m0 = m0.(*image.Paletted).SubImage(image.Rect(0, 0, 50, 30)) + var buf bytes.Buffer + err = Encode(&buf, m0, nil) + if err != nil { + t.Fatalf("Encode: %v", err) + } + m1, err := Decode(&buf) + if err != nil { + t.Fatalf("Decode: %v", err) + } + if m0.Bounds() != m1.Bounds() { + t.Fatalf("bounds differ: %v and %v", m0.Bounds(), m1.Bounds()) + } + if averageDelta(m0, m1) != 0 { + t.Fatalf("images differ") + } +} + +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.Fatal(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) + } +} |