From baf3814b293e183b2498fe984cdb431285efb3e2 Mon Sep 17 00:00:00 2001 From: Nigel Tao Date: Tue, 28 Apr 2015 15:33:03 +1000 Subject: [PATCH] image/gif: encode disposal, bg index and Config. The previous CL implemented decoding, but not encoding. Also return the global color map (if present) for DecodeConfig. Change-Id: I3b99c93720246010c9fe0924dc40a67875dfc852 Reviewed-on: https://go-review.googlesource.com/9389 Reviewed-by: Rob Pike --- src/image/gif/reader.go | 87 ++++++++++----------- src/image/gif/writer.go | 65 ++++++++++++---- src/image/gif/writer_test.go | 143 +++++++++++++++++++++++++++++++++-- 3 files changed, 222 insertions(+), 73 deletions(-) diff --git a/src/image/gif/reader.go b/src/image/gif/reader.go index b3ed0388f4f465..07adeb3a94dc58 100644 --- a/src/image/gif/reader.go +++ b/src/image/gif/reader.go @@ -32,15 +32,9 @@ type reader interface { // Masks etc. const ( // Fields. - fColorMapFollows = 1 << 7 - - // Screen Descriptor flags. - sdGlobalColorTable = 1 << 7 - - // Image fields. - ifLocalColorTable = 1 << 7 - ifInterlace = 1 << 6 - ifPixelSizeMask = 7 + fColorTable = 1 << 7 + fInterlace = 1 << 6 + fColorTableBitsMask = 7 // Graphic control flags. gcTransparentColorSet = 1 << 0 @@ -77,15 +71,11 @@ type decoder struct { vers string width int height int - headerFields byte - backgroundIndex byte loopCount int delayTime int + backgroundIndex byte disposalMethod byte - // Unused from header. - aspect byte - // From image descriptor. imageFields byte @@ -94,7 +84,6 @@ type decoder struct { hasTransparentIndex bool // Computed. - pixelSize uint globalColorMap color.Palette // Used when decoding. @@ -134,7 +123,7 @@ func (b *blockReader) Read(p []byte) (int, error) { b.err = io.EOF return 0, b.err } - b.slice = b.tmp[0:blockLen] + b.slice = b.tmp[:blockLen] if _, b.err = io.ReadFull(b.r, b.slice); b.err != nil { return 0, b.err } @@ -161,12 +150,6 @@ func (d *decoder) decode(r io.Reader, configOnly bool) error { 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 { @@ -183,9 +166,9 @@ func (d *decoder) decode(r io.Reader, configOnly bool) error { if err != nil { return err } - useLocalColorMap := d.imageFields&fColorMapFollows != 0 + useLocalColorMap := d.imageFields&fColorTable != 0 if useLocalColorMap { - m.Palette, err = d.readColorMap() + m.Palette, err = d.readColorMap(d.imageFields) if err != nil { return err } @@ -241,7 +224,7 @@ func (d *decoder) decode(r io.Reader, configOnly bool) error { } // Undo the interlacing if necessary. - if d.imageFields&ifInterlace != 0 { + if d.imageFields&fInterlace != 0 { uninterlace(m) } @@ -267,40 +250,35 @@ func (d *decoder) decode(r io.Reader, configOnly bool) error { } func (d *decoder) readHeaderAndScreenDescriptor() error { - _, err := io.ReadFull(d.r, d.tmp[0:13]) + _, err := io.ReadFull(d.r, d.tmp[:13]) if err != nil { return err } - d.vers = string(d.tmp[0:6]) + d.vers = string(d.tmp[: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] - if d.headerFields&sdGlobalColorTable != 0 { + if fields := d.tmp[10]; fields&fColorTable != 0 { d.backgroundIndex = d.tmp[11] + // readColorMap overwrites the contents of d.tmp, but that's OK. + if d.globalColorMap, err = d.readColorMap(fields); err != nil { + return err + } } - d.aspect = d.tmp[12] + // d.tmp[12] is the Pixel Aspect Ratio, which is ignored. 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]) +func (d *decoder) readColorMap(fields byte) (color.Palette, error) { + n := 1 << (1 + uint(fields&fColorTableBitsMask)) + _, err := io.ReadFull(d.r, d.tmp[:3*n]) if err != nil { return nil, fmt.Errorf("gif: short read on color map: %s", err) } - colorMap := make(color.Palette, numColors) + colorMap := make(color.Palette, n) j := 0 for i := range colorMap { colorMap[i] = color.RGBA{d.tmp[j+0], d.tmp[j+1], d.tmp[j+2], 0xFF} @@ -333,7 +311,7 @@ func (d *decoder) readExtension() error { return fmt.Errorf("gif: unknown extension 0x%.2x", extension) } if size > 0 { - if _, err := io.ReadFull(d.r, d.tmp[0:size]); err != nil { + if _, err := io.ReadFull(d.r, d.tmp[:size]); err != nil { return err } } @@ -358,7 +336,7 @@ func (d *decoder) readExtension() error { } func (d *decoder) readGraphicControl() error { - if _, err := io.ReadFull(d.r, d.tmp[0:6]); err != nil { + if _, err := io.ReadFull(d.r, d.tmp[:6]); err != nil { return fmt.Errorf("gif: can't read graphic control: %s", err) } flags := d.tmp[1] @@ -372,7 +350,7 @@ func (d *decoder) readGraphicControl() error { } func (d *decoder) newImageFromDescriptor() (*image.Paletted, error) { - if _, err := io.ReadFull(d.r, d.tmp[0:9]); err != nil { + if _, err := io.ReadFull(d.r, d.tmp[: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 @@ -396,7 +374,7 @@ func (d *decoder) readBlock() (int, error) { if n == 0 || err != nil { return 0, err } - return io.ReadFull(d.r, d.tmp[0:n]) + return io.ReadFull(d.r, d.tmp[:n]) } // interlaceScan defines the ordering for a pass of the interlace algorithm. @@ -444,10 +422,21 @@ func Decode(r io.Reader) (image.Image, error) { type GIF struct { Image []*image.Paletted // The successive images. Delay []int // The successive delay times, one per frame, in 100ths of a second. - Disposal []byte // The successive disposal methods, one per frame. LoopCount int // The loop count. - Config image.Config - // The background index in the Global Color Map. + // Disposal is the successive disposal methods, one per frame. For + // backwards compatibility, a nil Disposal is valid to pass to EncodeAll, + // and implies that each frame's disposal method is 0 (no disposal + // specified). + Disposal []byte + // Config is the global color map (palette), width and height. A nil or + // empty-color.Palette Config.ColorModel means that each frame has its own + // color map and there is no global color map. For backwards compatibility, + // a zero-valued Config is valid to pass to EncodeAll, and implies that the + // overall GIF's width and height equals the first frame's width and + // height. + Config image.Config + // BackgroundIndex is the background index in the global color map, for use + // with the DisposalBackground disposal method. BackgroundIndex byte } diff --git a/src/image/gif/writer.go b/src/image/gif/writer.go index 49abde704c8666..a70fc4079a440e 100644 --- a/src/image/gif/writer.go +++ b/src/image/gif/writer.go @@ -52,7 +52,7 @@ type encoder struct { w writer err error // g is a reference to the data that is being encoded. - g *GIF + g GIF // buf is a scratch buffer. It must be at least 768 so we can write the color map. buf [1024]byte } @@ -116,18 +116,26 @@ func (e *encoder) writeHeader() { 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())) + writeUint16(e.buf[0:2], uint16(e.g.Config.Width)) + writeUint16(e.buf[2:4], uint16(e.g.Config.Height)) 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]) + if p, ok := e.g.Config.ColorModel.(color.Palette); ok && len(p) > 0 { + paddedSize := log2(len(p)) // Size of Global Color Table: 2^(1+n). + e.buf[0] = fColorTable | uint8(paddedSize) + e.buf[1] = e.g.BackgroundIndex + e.buf[2] = 0x00 // Pixel Aspect Ratio. + e.write(e.buf[:3]) + e.writeColorTable(p, paddedSize) + } else { + // 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 { @@ -168,7 +176,7 @@ func (e *encoder) writeColorTable(p color.Palette, size int) { e.write(e.buf[:3*log2Lookup[size]]) } -func (e *encoder) writeImageBlock(pm *image.Paletted, delay int) { +func (e *encoder) writeImageBlock(pm *image.Paletted, delay int, disposal byte) { if e.err != nil { return } @@ -192,14 +200,14 @@ func (e *encoder) writeImageBlock(pm *image.Paletted, delay int) { } } - if delay > 0 || transparentIndex != -1 { + if delay > 0 || disposal != 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 + e.buf[3] = 0x01 | disposal<<2 } else { - e.buf[3] = 0x00 + e.buf[3] = 0x00 | disposal<<2 } writeUint16(e.buf[4:6], uint16(delay)) // Delay Time (1/100ths of a second) @@ -281,7 +289,23 @@ func EncodeAll(w io.Writer, g *GIF) error { g.LoopCount = 0 } - e := encoder{g: g} + e := encoder{g: *g} + // The GIF.Disposal, GIF.Config and GIF.BackgroundIndex fields were added + // in Go 1.5. Valid Go 1.4 code, such as when the Disposal field is omitted + // in a GIF struct literal, should still produce valid GIFs. + if e.g.Disposal != nil && len(e.g.Image) != len(e.g.Disposal) { + return errors.New("gif: mismatched image and disposal lengths") + } + if e.g.Config == (image.Config{}) { + b := g.Image[0].Bounds() + e.g.Config.Width = b.Dx() + e.g.Config.Height = b.Dy() + } else if e.g.Config.ColorModel != nil { + if _, ok := e.g.Config.ColorModel.(color.Palette); !ok { + return errors.New("gif: GIF color model must be a color.Palette") + } + } + if ww, ok := w.(writer); ok { e.w = ww } else { @@ -290,7 +314,11 @@ func EncodeAll(w io.Writer, g *GIF) error { e.writeHeader() for i, pm := range g.Image { - e.writeImageBlock(pm, g.Delay[i]) + disposal := uint8(0) + if g.Disposal != nil { + disposal = g.Disposal[i] + } + e.writeImageBlock(pm, g.Delay[i], disposal) } e.writeByte(sTrailer) e.flush() @@ -329,5 +357,10 @@ func Encode(w io.Writer, m image.Image, o *Options) error { return EncodeAll(w, &GIF{ Image: []*image.Paletted{pm}, Delay: []int{0}, + Config: image.Config{ + ColorModel: pm.Palette, + Width: b.Dx(), + Height: b.Dy(), + }, }) } diff --git a/src/image/gif/writer_test.go b/src/image/gif/writer_test.go index 93306ffdb34e3c..2248ac307a97e6 100644 --- a/src/image/gif/writer_test.go +++ b/src/image/gif/writer_test.go @@ -8,10 +8,12 @@ import ( "bytes" "image" "image/color" + "image/color/palette" _ "image/png" "io/ioutil" "math/rand" "os" + "reflect" "testing" ) @@ -125,55 +127,180 @@ func TestSubImage(t *testing.T) { } } +// palettesEqual reports whether two color.Palette values are equal, ignoring +// any trailing opaque-black palette entries. +func palettesEqual(p, q color.Palette) bool { + n := len(p) + if n > len(q) { + n = len(q) + } + for i := 0; i < n; i++ { + if p[i] != q[i] { + return false + } + } + for i := n; i < len(p); i++ { + r, g, b, a := p[i].RGBA() + if r != 0 || g != 0 || b != 0 || a != 0xffff { + return false + } + } + for i := n; i < len(q); i++ { + r, g, b, a := q[i].RGBA() + if r != 0 || g != 0 || b != 0 || a != 0xffff { + return false + } + } + return true +} + var frames = []string{ "../testdata/video-001.gif", "../testdata/video-005.gray.gif", } -func TestEncodeAll(t *testing.T) { +func testEncodeAll(t *testing.T, go1Dot5Fields bool, useGlobalColorModel bool) { + const width, height = 150, 103 + g0 := &GIF{ Image: make([]*image.Paletted, len(frames)), Delay: make([]int, len(frames)), LoopCount: 5, } for i, f := range frames { - m, err := readGIF(f) + g, err := readGIF(f) if err != nil { t.Fatal(f, err) } - g0.Image[i] = m.Image[0] + m := g.Image[0] + if m.Bounds().Dx() != width || m.Bounds().Dy() != height { + t.Fatalf("frame %d had unexpected bounds: got %v, want width/height = %d/%d", + i, m.Bounds(), width, height) + } + g0.Image[i] = m + } + // The GIF.Disposal, GIF.Config and GIF.BackgroundIndex fields were added + // in Go 1.5. Valid Go 1.4 or earlier code should still produce valid GIFs. + // + // On the following line, color.Model is an interface type, and + // color.Palette is a concrete (slice) type. + globalColorModel, backgroundIndex := color.Model(color.Palette(nil)), uint8(0) + if useGlobalColorModel { + globalColorModel, backgroundIndex = color.Palette(palette.WebSafe), uint8(1) } + if go1Dot5Fields { + g0.Disposal = make([]byte, len(g0.Image)) + for i := range g0.Disposal { + g0.Disposal[i] = DisposalNone + } + g0.Config = image.Config{ + ColorModel: globalColorModel, + Width: width, + Height: height, + } + g0.BackgroundIndex = backgroundIndex + } + var buf bytes.Buffer if err := EncodeAll(&buf, g0); err != nil { t.Fatal("EncodeAll:", err) } - g1, err := DecodeAll(&buf) + encoded := buf.Bytes() + config, err := DecodeConfig(bytes.NewReader(encoded)) + if err != nil { + t.Fatal("DecodeConfig:", err) + } + g1, err := DecodeAll(bytes.NewReader(encoded)) if err != nil { t.Fatal("DecodeAll:", err) } + + if !reflect.DeepEqual(config, g1.Config) { + t.Errorf("DecodeConfig inconsistent with DecodeAll") + } + if !palettesEqual(g1.Config.ColorModel.(color.Palette), globalColorModel.(color.Palette)) { + t.Errorf("unexpected global color model") + } + if w, h := g1.Config.Width, g1.Config.Height; w != width || h != height { + t.Errorf("got config width * height = %d * %d, want %d * %d", w, h, width, height) + } + if g0.LoopCount != g1.LoopCount { t.Errorf("loop counts differ: %d and %d", g0.LoopCount, g1.LoopCount) } + if backgroundIndex != g1.BackgroundIndex { + t.Errorf("background indexes differ: %d and %d", backgroundIndex, g1.BackgroundIndex) + } + if len(g0.Image) != len(g1.Image) { + t.Fatalf("image lengths differ: %d and %d", len(g0.Image), len(g1.Image)) + } + if len(g1.Image) != len(g1.Delay) { + t.Fatalf("image and delay lengths differ: %d and %d", len(g1.Image), len(g1.Delay)) + } + if len(g1.Image) != len(g1.Disposal) { + t.Fatalf("image and disposal lengths differ: %d and %d", len(g1.Image), len(g1.Disposal)) + } + 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()) + t.Errorf("frame %d: bounds differ: %v and %v", 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) + t.Errorf("frame %d: delay values differ: %d and %d", i, d0, d1) + } + p0, p1 := uint8(0), g1.Disposal[i] + if go1Dot5Fields { + p0 = DisposalNone + } + if p0 != p1 { + t.Errorf("frame %d: disposal values differ: %d and %d", i, p0, p1) } } +} - g1.Delay = make([]int, 1) - if err := EncodeAll(ioutil.Discard, g1); err == nil { +func TestEncodeAllGo1Dot4(t *testing.T) { testEncodeAll(t, false, false) } +func TestEncodeAllGo1Dot5(t *testing.T) { testEncodeAll(t, true, false) } +func TestEncodeAllGo1Dot5GlobalColorModel(t *testing.T) { testEncodeAll(t, true, true) } + +func TestEncodeMismatchDelay(t *testing.T) { + images := make([]*image.Paletted, 2) + for i := range images { + images[i] = image.NewPaletted(image.Rect(0, 0, 5, 5), palette.Plan9) + } + + g0 := &GIF{ + Image: images, + Delay: make([]int, 1), + } + if err := EncodeAll(ioutil.Discard, g0); err == nil { t.Error("expected error from mismatched delay and image slice lengths") } + + g1 := &GIF{ + Image: images, + Delay: make([]int, len(images)), + Disposal: make([]byte, 1), + } + for i := range g1.Disposal { + g1.Disposal[i] = DisposalNone + } + if err := EncodeAll(ioutil.Discard, g1); err == nil { + t.Error("expected error from mismatched disposal and image slice lengths") + } +} + +func TestEncodeZeroGIF(t *testing.T) { if err := EncodeAll(ioutil.Discard, &GIF{}); err == nil { t.Error("expected error from providing empty gif") } } +// TODO: add test for when individual frames are out of the global bounds. +// TODO: add test for when the first frame's bounds are not the same as the global bounds. +// TODO: add test for when a frame has the same color map (palette) as the global one. + func BenchmarkEncode(b *testing.B) { b.StopTimer()