mirror of
https://github.com/golang/go
synced 2024-11-18 11:04:42 -07:00
image/gif: add writer implementation
R=r, nigeltao CC=golang-dev https://golang.org/cl/10896043
This commit is contained in:
parent
bbb51ae3a9
commit
9ebc5be39c
@ -202,7 +202,7 @@ var pkgDeps = map[string][]string{
|
||||
"go/build": {"L4", "OS", "GOPARSER"},
|
||||
"html": {"L4"},
|
||||
"image/draw": {"L4"},
|
||||
"image/gif": {"L4", "compress/lzw"},
|
||||
"image/gif": {"L4", "compress/lzw", "image/draw"},
|
||||
"image/jpeg": {"L4"},
|
||||
"image/png": {"L4", "compress/zlib"},
|
||||
"index/suffixarray": {"L4", "regexp"},
|
||||
|
@ -2,7 +2,7 @@
|
||||
// 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.
|
||||
// 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
|
||||
|
329
src/pkg/image/gif/writer.go
Normal file
329
src/pkg/image/gif/writer.go
Normal file
@ -0,0 +1,329 @@
|
||||
// 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/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
|
||||
// bitsPerPixel is the number of bits required to represent each color
|
||||
// in the image.
|
||||
bitsPerPixel int
|
||||
// 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
|
||||
}
|
||||
|
||||
// TODO: This bases the global color table on the first image
|
||||
// only.
|
||||
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])
|
||||
|
||||
e.bitsPerPixel = log2(len(pm.Palette)) + 1
|
||||
e.buf[0] = 0x80 | ((uint8(e.bitsPerPixel) - 1) << 4) | (uint8(e.bitsPerPixel) - 1)
|
||||
e.buf[1] = 0x00 // Background Color Index.
|
||||
e.buf[2] = 0x00 // Pixel Aspect Ratio.
|
||||
e.write(e.buf[:3])
|
||||
|
||||
// Global Color Table.
|
||||
e.writeColorTable(pm.Palette, e.bitsPerPixel-1)
|
||||
|
||||
// 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 := e.bitsPerPixel
|
||||
if litWidth < 2 {
|
||||
litWidth = 2
|
||||
}
|
||||
e.writeByte(uint8(litWidth)) // LZW Minimum Code Size.
|
||||
|
||||
lzww := lzw.NewWriter(blockWriter{e: e}, lzw.LSB, litWidth)
|
||||
_, e.err = lzww.Write(pm.Pix)
|
||||
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.
|
||||
// color.Plan9Palette 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, color.Plan9Palette[: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},
|
||||
})
|
||||
}
|
204
src/pkg/image/gif/writer_test.go
Normal file
204
src/pkg/image/gif/writer_test.go
Normal file
@ -0,0 +1,204 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.Error(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)
|
||||
}
|
||||
}
|
BIN
src/pkg/image/testdata/video-005.gray.gif
vendored
Normal file
BIN
src/pkg/image/testdata/video-005.gray.gif
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Loading…
Reference in New Issue
Block a user