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>
// 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
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
hasTransparentIndex bool
// Computed.
- pixelSize uint
globalColorMap color.Palette
// Used when decoding.
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
}
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 {
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
}
}
// Undo the interlacing if necessary.
- if d.imageFields&ifInterlace != 0 {
+ if d.imageFields&fInterlace != 0 {
uninterlace(m)
}
}
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}
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
}
}
}
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]
}
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
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.
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
}
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
}
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 {
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
}
}
}
- 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)
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 {
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()
return EncodeAll(w, &GIF{
Image: []*image.Paletted{pm},
Delay: []int{0},
+ Config: image.Config{
+ ColorModel: pm.Palette,
+ Width: b.Dx(),
+ Height: b.Dy(),
+ },
})
}
"bytes"
"image"
"image/color"
+ "image/color/palette"
_ "image/png"
"io/ioutil"
"math/rand"
"os"
+ "reflect"
"testing"
)
}
}
+// 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()