A zero-dependency Go APNG (Animated PNG) encoder and decoder --- operates directly on zlib + PNG filter scanlines, avoiding the image/png round-trip.
- Zero dependencies --- standard library only, no third-party packages
- Full codec --- supports BlendOp (Source / Over), DisposeOp (None / Background / Previous)
- Temporal diffing --- only the changed bounding box between frames is encoded
- High performance --- direct
zlib+ PNG filter pipeline, nopng.Encode/png.Decodeoverhead - All color types --- decoder handles PNG color types 0–6, all bit depths, palette (PLTE), and 16-bit down-scaling
- Auto canvas --- zero
Width/Heighttriggers auto-calculation from frame bounds - Round-trip fidelity --- Encode then Decode preserves the original frame fragment semantics
go get github.com/xogas/apngpackage main
import (
"fmt"
"os"
"github.com/xogas/apng"
)
func main() {
f, _ := os.Open("input.png")
defer f.Close()
a, err := apng.Decode(f)
if err != nil {
panic(err)
}
fmt.Printf("%d frames, canvas %d×%d, loops %d\n",
len(a.Frames), a.Width, a.Height, a.LoopCount)
for i, frame := range a.Frames {
fmt.Printf("frame %d: %v, delay %v, dispose=%d blend=%d\n",
i, frame.Bounds(), frame.Delay(),
frame.DisposeOp, frame.BlendOp)
}
}package main
import (
"image"
"image/color"
"os"
"github.com/xogas/apng"
)
func main() {
red := image.NewRGBA(image.Rect(0, 0, 32, 32))
for y := 0; y < 32; y++ {
for x := 0; x < 32; x++ {
red.Set(x, y, color.RGBA{R: 255, A: 255})
}
}
green := image.NewRGBA(image.Rect(0, 0, 16, 16))
for y := 0; y < 16; y++ {
for x := 0; x < 16; x++ {
green.Set(x, y, color.RGBA{G: 255, A: 255})
}
}
a := &apng.APNG{
Width: 32,
Height: 32,
LoopCount: 0, // loop forever
Frames: []apng.Frame{
{Image: red, BlendOp: apng.BlendOpSource},
{Image: green, XOffset: 8, YOffset: 8, BlendOp: apng.BlendOpOver},
},
}
f, _ := os.Create("output.png")
defer f.Close()
if err := apng.Encode(a, f); err != nil {
panic(err)
}
}canvases := a.CompositeFrames()
for i, c := range canvases {
fmt.Printf("frame %d full canvas: %v\n", i, c.Bounds())
// c is an independent *image.RGBA --- modifying it does not affect a
}| Field | Type | Description |
|---|---|---|
Width |
uint32 |
Canvas width; auto-computed when zero (encode only) |
Height |
uint32 |
Canvas height; auto-computed when zero (encode only) |
Frames |
[]Frame |
Animation frames in display order (at least one required) |
LoopCount |
uint32 |
Number of loops; 0 = loop forever |
Background |
image.Image |
Optional static fallback for non-APNG viewers |
| Field | Type | Description |
|---|---|---|
Image |
image.Image |
Raw pixel fragment (not the composited full canvas) |
XOffset |
int |
Left offset of the frame on the canvas |
YOffset |
int |
Top offset of the frame on the canvas |
DelayNum |
uint16 |
Frame delay numerator |
DelayDen |
uint16 |
Frame delay denominator (seconds); 0 is treated as 100 |
DisposeOp |
DisposeOp |
Canvas disposal after this frame is displayed |
BlendOp |
BlendOp |
How the frame is composited onto the canvas |
| Constant | Value | Description |
|---|---|---|
DisposeOpNone |
0 | Leave the canvas as-is (default) |
DisposeOpBackground |
1 | Clear the frame region to transparent black |
DisposeOpPrevious |
2 | Restore the frame region to its pre-render state |
| Constant | Value | Description |
|---|---|---|
BlendOpSource |
0 | Replace canvas pixels directly (default) |
BlendOpOver |
1 | Alpha-blend onto the canvas |
func (f *Frame) Bounds() image.Rectangle // frame region in canvas coordinates
func (f *Frame) Delay() time.Duration // display duration of this frame
func (a *APNG) CompositeFrames() []*image.RGBA // full canvas per frame after compositing// Format / I/O errors
var (
ErrInvalidSignature // invalid PNG signature
ErrNotAPNG // missing acTL chunk (not an APNG file)
ErrCRCMismatch // CRC-32 checksum mismatch
ErrInvalidChunk // malformed chunk
)
// Input validation errors
var (
ErrNoFrames // no frames to encode
ErrNilImage // frame has nil Image
ErrNegativeOffset // frame has negative offset
ErrZeroCanvas // canvas size is zero
ErrZeroImage // image has zero dimensions
)Use errors.Is(err, apng.ErrNotAPNG) to test error types.
The encoder converts images to RGBA, picks the best PNG filter per scanline (None / Sub / Up / Average / Paeth), and feeds the result through compress/zlib before wrapping in IDAT / fdAT chunks. The decoder reverses this: zlib decompress → unfilter → color-space conversion to *image.RGBA. No synthetic PNG scaffolding, no image/png round-trip.
The MIT License (MIT) --- see license