Commit baf3814b authored by Nigel Tao's avatar Nigel Tao

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/9389Reviewed-by: default avatarRob Pike <r@golang.org>
parent 0c62c93a
......@@ -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
}
......
......@@ -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(),
},
})
}
......@@ -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()
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment