Skip to content

Instantly share code, notes, and snippets.

@imjasonh
Last active October 28, 2021 06:23
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save imjasonh/7b309a4af2d4e32a2649 to your computer and use it in GitHub Desktop.
Save imjasonh/7b309a4af2d4e32a2649 to your computer and use it in GitHub Desktop.
Incremental GIF writer (heavily borrowed from standard library's gif.EncodeAll) -- allows frames to be added on demand
// 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 supergif
import (
"bufio"
"compress/lzw"
"errors"
"image"
"image/color"
"io"
)
// Graphic control extension fields.
const (
gcLabel = 0xF9
gcBlockSize = 0x04
)
// Section indicators.
const (
sExtension = 0x21
sImageDescriptor = 0x2C
sTrailer = 0x3B
)
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
// 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(pm *image.Paletted) {
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(pm.Bounds().Dx()))
writeUint16(e.buf[2:4], uint16(pm.Bounds().Dy()))
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])
// Add animation info.
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(0)) // 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 := paddedSize + 1
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.
}
type IncrementalEncoder interface {
EncodeNext(*image.Paletted) error
Finish() error
}
type incremental struct {
w io.Writer
e encoder
}
func NewIncrementalEncoder(w io.Writer, pm *image.Paletted) (IncrementalEncoder, error) {
i := incremental{w: w, e: encoder{}}
if ww, ok := i.w.(writer); ok {
i.e.w = ww
} else {
i.e.w = bufio.NewWriter(w)
}
i.e.writeHeader(pm)
i.e.flush()
return &i, i.e.err
}
func (i *incremental) EncodeNext(pm *image.Paletted) error {
i.e.writeImageBlock(pm, 0)
i.e.flush()
return i.e.err
}
func (i *incremental) Finish() error {
i.e.writeByte(sTrailer)
i.e.flush()
return i.e.err
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment