]> Cypherpunks repositories - gostls13.git/commitdiff
image/gif: encode disposal, bg index and Config.
authorNigel Tao <nigeltao@golang.org>
Tue, 28 Apr 2015 05:33:03 +0000 (15:33 +1000)
committerNigel Tao <nigeltao@golang.org>
Tue, 28 Apr 2015 23:01:37 +0000 (23:01 +0000)
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 <r@golang.org>
src/image/gif/reader.go
src/image/gif/writer.go
src/image/gif/writer_test.go

index b3ed0388f4f4657f8041180ac75ab1d3018c6e11..07adeb3a94dc58e1142dc03d3704425be31aa048 100644 (file)
@@ -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
 }
 
index 49abde704c86669af12714da0bc505d3ceae4492..a70fc4079a440eec184812a31c4f7826e93bc7e9 100644 (file)
@@ -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(),
+               },
        })
 }
index 93306ffdb34e3c3d34edc4fa91d98c5c4e88a2fe..2248ac307a97e6bb645de27adf9bf1018d6d9260 100644 (file)
@@ -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()