Last active
October 28, 2021 06:23
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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