mirror of
https://github.com/autc04/Retro68.git
synced 2024-09-08 20:55:01 +00:00
468 lines
12 KiB
Go
468 lines
12 KiB
Go
// 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"
|
|
"bytes"
|
|
"compress/lzw"
|
|
"errors"
|
|
"image"
|
|
"image/color"
|
|
"image/color/palette"
|
|
"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
|
|
// globalCT is the size in bytes of the global color table.
|
|
globalCT int
|
|
// buf is a scratch buffer. It must be at least 256 for the blockWriter.
|
|
buf [256]byte
|
|
globalColorTable [3 * 256]byte
|
|
localColorTable [3 * 256]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) setup() {
|
|
b.e.buf[0] = 0
|
|
}
|
|
|
|
func (b blockWriter) Flush() error {
|
|
return b.e.err
|
|
}
|
|
|
|
func (b blockWriter) WriteByte(c byte) error {
|
|
if b.e.err != nil {
|
|
return b.e.err
|
|
}
|
|
|
|
// Append c to buffered sub-block.
|
|
b.e.buf[0]++
|
|
b.e.buf[b.e.buf[0]] = c
|
|
if b.e.buf[0] < 255 {
|
|
return nil
|
|
}
|
|
|
|
// Flush block
|
|
b.e.write(b.e.buf[:256])
|
|
b.e.buf[0] = 0
|
|
return b.e.err
|
|
}
|
|
|
|
// blockWriter must be an io.Writer for lzw.NewWriter, but this is never
|
|
// actually called.
|
|
func (b blockWriter) Write(data []byte) (int, error) {
|
|
for i, c := range data {
|
|
if err := b.WriteByte(c); err != nil {
|
|
return i, err
|
|
}
|
|
}
|
|
return len(data), nil
|
|
}
|
|
|
|
func (b blockWriter) close() {
|
|
// Write the block terminator (0x00), either by itself, or along with a
|
|
// pending sub-block.
|
|
if b.e.buf[0] == 0 {
|
|
b.e.writeByte(0)
|
|
} else {
|
|
n := uint(b.e.buf[0])
|
|
b.e.buf[n+1] = 0
|
|
b.e.write(b.e.buf[:n+2])
|
|
}
|
|
b.e.flush()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Logical screen width and height.
|
|
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])
|
|
|
|
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])
|
|
var err error
|
|
e.globalCT, err = encodeColorTable(e.globalColorTable[:], p, paddedSize)
|
|
if err != nil && e.err == nil {
|
|
e.err = err
|
|
return
|
|
}
|
|
e.write(e.globalColorTable[:e.globalCT])
|
|
} 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.buf[0] = 0x21 // Extension Introducer.
|
|
e.buf[1] = 0xff // Application Label.
|
|
e.buf[2] = 0x0b // Block Size.
|
|
e.write(e.buf[:3])
|
|
_, err := io.WriteString(e.w, "NETSCAPE2.0") // Application Identifier.
|
|
if err != nil && e.err == nil {
|
|
e.err = err
|
|
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 encodeColorTable(dst []byte, p color.Palette, size int) (int, error) {
|
|
if uint(size) >= uint(len(log2Lookup)) {
|
|
return 0, errors.New("gif: cannot encode color table with more than 256 entries")
|
|
}
|
|
for i, c := range p {
|
|
if c == nil {
|
|
return 0, errors.New("gif: cannot encode color table with nil entries")
|
|
}
|
|
var r, g, b uint8
|
|
// It is most likely that the palette is full of color.RGBAs, so they
|
|
// get a fast path.
|
|
if rgba, ok := c.(color.RGBA); ok {
|
|
r, g, b = rgba.R, rgba.G, rgba.B
|
|
} else {
|
|
rr, gg, bb, _ := c.RGBA()
|
|
r, g, b = uint8(rr>>8), uint8(gg>>8), uint8(bb>>8)
|
|
}
|
|
dst[3*i+0] = r
|
|
dst[3*i+1] = g
|
|
dst[3*i+2] = b
|
|
}
|
|
n := log2Lookup[size]
|
|
if n > len(p) {
|
|
// Pad with black.
|
|
fill := dst[3*len(p) : 3*n]
|
|
for i := range fill {
|
|
fill[i] = 0
|
|
}
|
|
}
|
|
return 3 * n, nil
|
|
}
|
|
|
|
func (e *encoder) colorTablesMatch(localLen, transparentIndex int) bool {
|
|
localSize := 3 * localLen
|
|
if transparentIndex >= 0 {
|
|
trOff := 3 * transparentIndex
|
|
return bytes.Equal(e.globalColorTable[:trOff], e.localColorTable[:trOff]) &&
|
|
bytes.Equal(e.globalColorTable[trOff+3:localSize], e.localColorTable[trOff+3:localSize])
|
|
}
|
|
return bytes.Equal(e.globalColorTable[:localSize], e.localColorTable[:localSize])
|
|
}
|
|
|
|
func (e *encoder) writeImageBlock(pm *image.Paletted, delay int, disposal byte) {
|
|
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.Min.X < 0 || b.Max.X >= 1<<16 || b.Min.Y < 0 || b.Max.Y >= 1<<16 {
|
|
e.err = errors.New("gif: image block is too large to encode")
|
|
return
|
|
}
|
|
if !b.In(image.Rectangle{Max: image.Point{e.g.Config.Width, e.g.Config.Height}}) {
|
|
e.err = errors.New("gif: image block is out of bounds")
|
|
return
|
|
}
|
|
|
|
transparentIndex := -1
|
|
for i, c := range pm.Palette {
|
|
if c == nil {
|
|
e.err = errors.New("gif: cannot encode color table with nil entries")
|
|
return
|
|
}
|
|
if _, _, _, a := c.RGBA(); a == 0 {
|
|
transparentIndex = i
|
|
break
|
|
}
|
|
}
|
|
|
|
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 | disposal<<2
|
|
} else {
|
|
e.buf[3] = 0x00 | disposal<<2
|
|
}
|
|
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])
|
|
|
|
// To determine whether or not this frame's palette is the same as the
|
|
// global palette, we can check a couple things. First, do they actually
|
|
// point to the same []color.Color? If so, they are equal so long as the
|
|
// frame's palette is not longer than the global palette...
|
|
paddedSize := log2(len(pm.Palette)) // Size of Local Color Table: 2^(1+n).
|
|
if gp, ok := e.g.Config.ColorModel.(color.Palette); ok && len(pm.Palette) <= len(gp) && &gp[0] == &pm.Palette[0] {
|
|
e.writeByte(0) // Use the global color table.
|
|
} else {
|
|
ct, err := encodeColorTable(e.localColorTable[:], pm.Palette, paddedSize)
|
|
if err != nil {
|
|
if e.err == nil {
|
|
e.err = err
|
|
}
|
|
return
|
|
}
|
|
// This frame's palette is not the very same slice as the global
|
|
// palette, but it might be a copy, possibly with one value turned into
|
|
// transparency by DecodeAll.
|
|
if ct <= e.globalCT && e.colorTablesMatch(len(pm.Palette), transparentIndex) {
|
|
e.writeByte(0) // Use the global color table.
|
|
} else {
|
|
// Use a local color table.
|
|
e.writeByte(fColorTable | uint8(paddedSize))
|
|
e.write(e.localColorTable[:ct])
|
|
}
|
|
}
|
|
|
|
litWidth := paddedSize + 1
|
|
if litWidth < 2 {
|
|
litWidth = 2
|
|
}
|
|
e.writeByte(uint8(litWidth)) // LZW Minimum Code Size.
|
|
|
|
bw := blockWriter{e: e}
|
|
bw.setup()
|
|
lzww := lzw.NewWriter(bw, lzw.LSB, litWidth)
|
|
if dx := b.Dx(); dx == pm.Stride {
|
|
_, e.err = lzww.Write(pm.Pix[:dx*b.Dy()])
|
|
if e.err != nil {
|
|
lzww.Close()
|
|
return
|
|
}
|
|
} else {
|
|
for i, y := 0, b.Min.Y; y < b.Max.Y; i, y = i+pm.Stride, y+1 {
|
|
_, e.err = lzww.Write(pm.Pix[i : i+dx])
|
|
if e.err != nil {
|
|
lzww.Close()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
lzww.Close() // flush to bw
|
|
bw.close() // flush to e.w
|
|
}
|
|
|
|
// 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.
|
|
// palette.Plan9 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}
|
|
// 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{}) {
|
|
p := g.Image[0].Bounds().Max
|
|
e.g.Config.Width = p.X
|
|
e.g.Config.Height = p.Y
|
|
} 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.w = bufio.NewWriter(w)
|
|
}
|
|
|
|
e.writeHeader()
|
|
for i, pm := range g.Image {
|
|
disposal := uint8(0)
|
|
if g.Disposal != nil {
|
|
disposal = g.Disposal[i]
|
|
}
|
|
e.writeImageBlock(pm, g.Delay[i], disposal)
|
|
}
|
|
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, palette.Plan9[: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)
|
|
}
|
|
|
|
// When calling Encode instead of EncodeAll, the single-frame image is
|
|
// translated such that its top-left corner is (0, 0), so that the single
|
|
// frame completely fills the overall GIF's bounds.
|
|
if pm.Rect.Min != (image.Point{}) {
|
|
dup := *pm
|
|
dup.Rect = dup.Rect.Sub(dup.Rect.Min)
|
|
pm = &dup
|
|
}
|
|
|
|
return EncodeAll(w, &GIF{
|
|
Image: []*image.Paletted{pm},
|
|
Delay: []int{0},
|
|
Config: image.Config{
|
|
ColorModel: pm.Palette,
|
|
Width: b.Dx(),
|
|
Height: b.Dy(),
|
|
},
|
|
})
|
|
}
|