Browse Source

What a mess

master
maze 2 years ago
parent
commit
1de2de5003
  1. 134
      animation.go
  2. 17
      animation_pin2dmd.go
  3. 66
      animation_rundmd.go
  4. 55
      animation_test.go
  5. 260
      animation_vni.go
  6. 52
      animationset.go
  7. 148
      animationset_pin2dmd.go
  8. 281
      animationset_vni.go
  9. 67
      animationset_vni_test.go
  10. 110
      checksum.go
  11. 50
      cmd/dmd-pal/main.go
  12. 154
      cmd/dmd2gif/main.go
  13. 373
      colorize.go
  14. 0
      colorize_test.go
  15. 35
      frame.go
  16. 252
      palette.go
  17. 95
      util.go

134
animation.go

@ -6,9 +6,7 @@ import (
"image"
"image/color"
"image/gif"
"io"
"log"
"strings"
"time"
)
@ -37,14 +35,20 @@ type Animation struct {
// Frame data.
Frame []*Frame
// Masks data.
Masks [][]byte
// Palettes data.
Palettes map[uint8]color.Palette // Frame palette (if any).
Palettes map[int]color.Palette // Frame palette (if any).
// ClockOffset location data.
ClockOffset image.Point
// Clock location data.
Clock []*image.Point
// ClockInFront renders the clock in front of the frame.
ClockInFront bool
// ClockSmall displays a small clock for a given index.
ClockSmall []bool
ClockSmall bool
}
func (a Animation) Duration() time.Duration {
@ -63,97 +67,65 @@ func (a Animation) Duration() time.Duration {
return sum
}
func (a Animation) GIF(coloring *Coloring, scale uint) *gif.GIF {
if scale <= 0 {
scale = 1
}
func (a *Animation) GIF(palette color.Palette, scale uint) *gif.GIF {
return a.generateGIF(nil, palette, scale)
}
size := len(a.Index)
if size == 0 {
size = len(a.Frame)
}
func (a *Animation) ColoredGIF(coloring *Coloring, scale uint) *gif.GIF {
return a.generateGIF(coloring, nil, scale)
}
g := &gif.GIF{
Image: make([]*image.Paletted, size),
Delay: make([]int, size),
func (a *Animation) generateGIF(coloring *Coloring, overridePalette color.Palette, scale uint) *gif.GIF {
if scale <= 0 {
scale = 1
}
var palette color.Palette
if coloring != nil && coloring.Palette != nil {
palette = coloring.Palette.Colors
g := new(gif.GIF)
if coloring != nil && false {
log.Println("animation using colorizer")
colorizer := NewColorizer(coloring)
colorized := colorizer.Colorize(a)
for _, i := range a.Index {
g.Delay = append(g.Delay, int(a.Frame[i].Delay/time.Millisecond/10))
g.Image = append(g.Image, toPaletted(Panel{
PalettedImage: colorized[i],
Dot: int(scale),
}, colorized[i].Palette))
}
} else {
palette = Amber
}
if len(a.Index) == 0 {
// Linear scan.
for i, f := range a.Frame {
if coloring != nil {
if m, ok := coloring.Mappings[f.Checksum]; ok {
palette = coloring.Palettes[m.PaletteIndex].Colors
log.Printf("%s: frame %#08x using custom palette %v", a.Name, f.Checksum, palette)
} else {
log.Printf("%s: frame %#08x has no palette mapping", a.Name, f.Checksum)
}
p := overridePalette
if p == nil && a.PaletteIndex > -1 {
log.Printf("animation palette index: %d", a.PaletteIndex)
p = a.Palettes[a.PaletteIndex]
}
if p == nil {
if a.Palette != nil {
log.Println("animation using default animation palette")
p = a.Palette
} else {
p = Amber
}
g.Delay[i] = int(f.Delay / time.Millisecond / 10)
g.Image[i] = toPaletted(Panel{
PalettedImage: &FrameImage{
Frame: f,
Palette: palette,
},
Dot: int(scale),
}, palette)
}
} else {
log.Printf("palette: %#+v", p)
for j, i := range a.Index {
if i < 0 {
continue
}
f := a.Frame[i]
if coloring != nil {
if m, ok := coloring.Mappings[f.Checksum]; ok {
palette = coloring.Palettes[m.PaletteIndex].Colors
log.Printf("%s: frame %#08x using custom palette %v", a.Name, f.Checksum, palette)
} else {
log.Printf("frame %#08x has no palette mapping", f.Checksum)
}
if f == nil {
panic(fmt.Sprintf("index %d points to %d which has a nil frame!", i, j))
}
g.Delay[j] = int(f.Delay / time.Millisecond / 10)
g.Image[i] = toPaletted(Panel{
g.Delay = append(g.Delay, int(f.Delay/time.Millisecond/10))
g.Image = append(g.Image, toPaletted(Panel{
PalettedImage: &FrameImage{
Frame: f,
Palette: palette,
Palette: p,
},
Dot: int(scale),
}, palette)
}, p))
}
}
return g
}
func ReadAnimations(reader io.ReadSeeker) ([]*Animation, error) {
if err := rewind(reader); err != nil {
return nil, err
}
var magic [4]byte
if _, err := io.ReadFull(reader, magic[:]); err != nil {
return nil, err
}
id := cString(magic[:])
switch {
case id == "ANIM":
return readPin2DMDAnimations(reader)
case id == "VPIN":
return readVPINAnimations(reader)
case strings.HasPrefix(id, runDMDHeaderMagic):
return readRunDMDAnimations(reader)
case id == "\x11HDD":
return nil, errors.New("dmd: please unpack the HDD Raw Copy image to raw (dd) format")
default:
return nil, fmt.Errorf("dmd: magic %q does not denote a supported format", cString(magic[:]))
}
}

17
animation_pin2dmd.go

@ -3,6 +3,7 @@ package dmd
import (
"bufio"
"bytes"
"encoding/binary"
"fmt"
"image/color"
"io"
@ -79,7 +80,7 @@ func readPin2DMDAnimations(rs io.ReadSeeker) (all []*Animation, err error) {
r := bufferedReader(rs)
for i := 0; i < int(h.Animations); i++ {
a := &Animation{
Palettes: make(map[uint8]color.Palette),
Palettes: make(map[int]color.Palette),
}
if err = a.readPin2DMDData(r, int(h.Version)); err != nil {
return
@ -139,7 +140,7 @@ func (a *Animation) readPin2DMDData(r *bufio.Reader, version int) (err error) {
//if a.Name == "Tilt" {
log.Printf("custom palette %d in %q: %d colors: %+v", index, a.Name, len(palette), palette)
//}
a.Palettes[index] = palette
a.Palettes[int(index)] = palette
}
if version >= 3 {
@ -180,7 +181,7 @@ func (a *Animation) readPin2DMDData(r *bufio.Reader, version int) (err error) {
}
if recorded != 0 {
// Link to the recording (ignored).
if _, err = readBEString(r); err != nil {
if _, err = readString(r); err != nil {
return
}
// Start frame (ignored)
@ -233,10 +234,7 @@ func (a *Animation) readPin2DMDFrames(r *bufio.Reader, n, version int) (err erro
a.Frame = make([]*Frame, n)
for i := 0; i < n; i++ {
// log.Printf("pin2dmd: frame %d/%d", i+1, n)
a.Frame[i] = &Frame{
Width: a.Width,
Height: a.Height,
}
a.Frame[i] = NewFrame(a.Width, a.Height)
a.Index[i] = i
if err = a.Frame[i].readPin2DMDData(r, version); err != nil {
return fmt.Errorf("error decoding frame: %w", err)
@ -264,10 +262,11 @@ func (f *Frame) readPin2DMDData(r *bufio.Reader, version int) (err error) {
// log.Println(f.Delay)
if version >= 4 {
f.Hash = make([]byte, 4)
if _, err = io.ReadFull(r, f.Hash); err != nil {
var hash [4]byte
if _, err = io.ReadFull(r, hash[:]); err != nil {
return
}
f.Checksum = binary.BigEndian.Uint32(hash[:])
}
var nPlanes uint8

66
animation_rundmd.go

@ -2,6 +2,7 @@ package dmd
import (
"io"
"log"
"time"
)
@ -61,6 +62,7 @@ func (a *Animation) readRunDMDData(rs io.ReadSeeker, n int) (err error) {
a.Name = cString(h.Name[:])
a.Width = int(h.Width)
a.Height = int(h.Height)
log.Printf("rundmd: animation %q: %#+v", a.Name, h)
if _, err = rs.Seek(int64(h.ByteOffset)*runDMDHeaderSize, io.SeekStart); err != nil {
return
@ -88,26 +90,66 @@ func (a *Animation) readRunDMDData(rs io.ReadSeeker, n int) (err error) {
return
}
// Read interleaved (4-bits) palette indexes.
f := NewFrame(a.Width, a.Height)
f.Delay = delays[i]
// Read interleaved (4-bits) planes.
if _, err = io.ReadFull(rs, b); err != nil {
return
}
// Deinterleave.
p := &Plane{
Data: make([]byte, a.Width*a.Height),
// Samples are packed plane values (one nibble contains bits for each plane):
// 0x2a -> 0010 (upper) 1010 (lower)
// planes: 0123 0123
//
// 0x87654321 -> 1000 0111 0110 0101 0100 0011 0010 0001
// plane0 10000000
// plane1 01111000
// plane2 01100110
// plane3 01010101
f.Plane = make([]*Plane, 4)
for j := 0; j < 4; j++ {
f.Plane[j] = &Plane{
Marker: uint8(j),
Data: make([]byte, (a.Width*a.Height)>>2),
}
}
for j, v := range b {
q := j << 1
p.Data[q+0] = v >> 4
p.Data[q+1] = v & 0x0f
var (
index = j >> 2
bit0 = (3 - (j & 3)) << 1 // bits 6, 4, 2, 0
bit1 = bit0 | 1 // bits 7, 5, 3, 1
)
f.Plane[0].Data[index] |= ((v & 0x01) >> 0) << bit0
f.Plane[1].Data[index] |= ((v & 0x02) >> 1) << bit0
f.Plane[2].Data[index] |= ((v & 0x04) >> 2) << bit0
f.Plane[3].Data[index] |= ((v & 0x08) >> 3) << bit0
f.Plane[0].Data[index] |= ((v & 0x10) >> 4) << bit1
f.Plane[1].Data[index] |= ((v & 0x20) >> 5) << bit1
f.Plane[2].Data[index] |= ((v & 0x40) >> 6) << bit1
f.Plane[3].Data[index] |= ((v & 0x80) >> 7) << bit1
}
a.Frame = append(a.Frame, &Frame{
Delay: delays[i],
Bits: 4,
Plane: []*Plane{p},
})
// Deinterleave.
/*
data := make([]byte, a.Width*a.Height)
for j, v := range b {
q := j << 1
data[q+0] = v >> 4
data[q+1] = v & 0x0f
}
for j := 0; j < 4; j++ {
p := &Plane{
Marker: byte(j),
Data: make([]byte, a.Width*a.Height/8),
}
f.Plane = append(f.Plane, p)
}
*/
a.Frame = append(a.Frame, f)
}
return

55
animation_test.go

@ -106,7 +106,7 @@ func testSaveGIF(t *testing.T, name string, a *Animation, c *Coloring) error {
return err
}
if err := gif.EncodeAll(o, a.GIF(c, 1)); err != nil {
if err := gif.EncodeAll(o, a.GIF(c, 5)); err != nil {
_ = o.Close()
_ = os.Remove(name)
return err
@ -134,59 +134,6 @@ func TestReadRunDMDAnimations(t *testing.T) {
t.Logf("%s: %d animations", name, len(all))
}
func TestReadVNIAnimations(t *testing.T) {
tests := []struct {
Name string
Palette string
}{
{
Name: "ID4_1.00.vni",
Palette: "ID4_1.00.pal",
},
{
Name: "sprk_103.vni",
Palette: "sprk_103.pal",
},
}
for _, test := range tests {
t.Run(test.Name, func(it *testing.T) {
f, err := os.Open(filepath.Join("testdata", test.Name))
if err != nil {
it.Skip(err)
}
defer f.Close()
all, err := ReadAnimations(f)
if err != nil {
it.Fatal(err)
}
it.Logf("%s: %d animations", test.Name, len(all))
var c *Coloring
if test.Palette != "" {
if c, err = LoadColoring(filepath.Join("testdata", test.Palette)); err != nil {
it.Fatal(err)
}
it.Logf("%s: coloring: %d palettes, %d mappings", test.Name, len(c.Palettes), len(c.Mappings))
}
for i, a := range all {
it.Logf("%s: animation %d: %d frames, duration %s", test.Name, i, len(a.Frame), a.Duration())
if len(a.Frame) > 1 {
/*
for _, f := range a.Frame {
for _, p := range f.Plane {
dumpPlane(p.Data, f.Width/8)
}
}
*/
testSaveGIF(t, test.Name, a, c)
}
}
})
}
}
func dumpPlane(plane []byte, bytesPerLine int) {
var sb strings.Builder
for i, l := 0, len(plane); i < l; i++ {

260
animation_vni.go

@ -1,55 +1,55 @@
package dmd
import (
"bufio"
"encoding/binary"
"image/color"
"io"
"io/ioutil"
"log"
"time"
)
/*
VNI animation file
The VNI format contains 0 or more animations, consisting of frames with a delay
and multiple planes. Each animation has markers for clock locations and sizes.
*/
/*
type vniHeader struct {
Version int16
Animations int16
}
type vniAnimationHeader struct {
Cycles int16
Hold int16
ClockFrom int16
ClockSmall uint8
ClockInFront uint8
ClockSmall bool
ClockInFront bool
ClockOffsetX int16
ClockOffsetY int16
RefreshDelay uint16
RefreshDelay int16
Type uint8
Fsk uint8
Frames uint16
Frames int16
}
func readVPINAnimations(reader io.Reader) (all []*Animation, err error) {
var (
r = bufferedReader(reader)
version uint16
nAnimations int16
)
if err = binary.Read(r, binary.BigEndian, &version); err != nil {
return nil, err
}
if err = binary.Read(r, binary.BigEndian, &nAnimations); err != nil {
return nil, err
func readVPINAnimations(r io.Reader) (all []*Animation, err error) {
r = newReadDumper(r)
var header vniHeader
if err = readBE(r, &header); err != nil {
return
}
log.Printf("VPI file version %d, %d animations", header.Version, header.Animations)
if version >= 2 {
// Skip animation indexes (why?)
log.Printf("skipping %d bytes of animation indexes", int(nAnimations)*4)
if _, err = io.Copy(ioutil.Discard, io.LimitReader(r, int64(nAnimations)*4)); err != nil {
return
if header.Version >= 2 {
// Skip animation indexes, we'll get to them as we parse the .VNI file.ß
log.Printf("skipping %d bytes of animation indexes", header.Animations*4)
for i := 0; i < int(header.Animations); i++ {
if _, err = readUint32(r); err != nil {
return
}
}
}
log.Printf("VPI file version %d, %d animations", version, nAnimations)
for i := 0; i < int(nAnimations); i++ {
all = make([]*Animation, header.Animations)
for i := 0; i < int(header.Animations); i++ {
a := new(Animation)
if err = a.readVPIData(r, int(version)); err != nil {
if err = a.readVPIData(r, int(header.Version)); err != nil {
return
}
all = append(all, a)
@ -57,114 +57,99 @@ func readVPINAnimations(reader io.Reader) (all []*Animation, err error) {
return all, nil
}
func (a *Animation) readVPIData(r *bufio.Reader, version int) (err error) {
var (
nameLength uint16
nameBytes []byte
header vniAnimationHeader
)
if err = binary.Read(r, binary.BigEndian, &nameLength); err != nil {
func (a *Animation) readVPIData(r io.Reader, version int) (err error) {
if a.Name, err = readString(r); err != nil {
return
}
if nameLength > 0 {
nameBytes = make([]byte, nameLength)
if _, err = io.ReadFull(r, nameBytes); err != nil {
return
}
a.Name = cString(nameBytes)
}
var header vniAnimationHeader
if err = binary.Read(r, binary.BigEndian, &header); err != nil {
return
}
frames := int(header.Frames)
if frames < 0 {
frames += 1 << 16
}
log.Printf("reading VNI animation %q version %d: %#+v", a.Name, version, header)
log.Printf("reading VNI animation %q version %d with %d frames: %#+v", a.Name, version, frames, header)
a.ClockOffset = image.Pt(int(header.ClockOffsetX), int(header.ClockOffsetY))
a.ClockInFront = header.ClockInFront
a.ClockSmall = header.ClockSmall
if version >= 2 {
// Read palette data.
if a.PaletteIndex, a.Palette, err = readVPIPalette(r); err != nil {
return
}
log.Printf("%s: palette index %d, custom palette: %t (%d)", a.Name, a.PaletteIndex, a.Palette != nil, len(a.Palette))
if a.PaletteIndex > -1 {
log.Printf("%s: palette index %d, custom palette: %t (%d)", a.Name, a.PaletteIndex, a.Palette != nil, len(a.Palette))
}
} else {
a.PaletteIndex = -1
}
if version >= 3 {
// Edit mode.
if _, err = r.ReadByte(); err != nil {
if _, err = readBool(r); err != nil {
return
}
}
if version >= 4 {
if a.Width, a.Height, err = readVPIDimensions(r); err != nil {
var size [2]int16
if err = readBE(r, &size); err != nil {
return
}
}
if err = readVPIFrames(r, a, int(header.Frames), version); err != nil {
return
a.Width = int(size[0])
a.Height = int(size[1])
log.Printf("%s: frame size %dx%d", a.Name, a.Width, a.Height)
}
// linear indexes
a.Index = make([]int, len(a.Frame))
for i := range a.Frame {
a.Index[i] = i
if version >= 5 {
// TODO: read masks
}
return
}
if version >= 6 {
// TODO: read links
}
func readVPIFrames(r *bufio.Reader, a *Animation, n int, version int) (err error) {
for i := 0; i < n; i++ {
f := &Frame{
Width: a.Width,
Height: a.Height,
}
for i := 0; i < frames; i++ {
f := NewFrame(a.Width, a.Height)
if err = f.readVNIData(r, version); err != nil {
return
}
a.Frame = append(a.Frame, f)
a.Index = append(a.Index, i)
}
return
}
func readVPIDimensions(r *bufio.Reader) (int, int, error) {
var w, h uint16
if err := binary.Read(r, binary.BigEndian, &w); err != nil {
return 0, 0, err
}
if err := binary.Read(r, binary.BigEndian, &h); err != nil {
return 0, 0, err
}
return int(w), int(h), nil
type vpiPaletteHeader struct {
Index int16
Colors int16
}
func readVPIPalette(r *bufio.Reader) (int, color.Palette, error) {
var i, n int16
if err := binary.Read(r, binary.BigEndian, &i); err != nil {
return -1, nil, err
}
if err := binary.Read(r, binary.BigEndian, &n); err != nil {
return -1, nil, err
func readVPIPalette(r io.Reader) (index int, palette color.Palette, err error) {
var header vpiPaletteHeader
if err = readBE(r, &header); err != nil {
return
}
if n <= 0 {
return int(i), nil, nil
if header.Colors <= 0 {
return -1, nil, nil
}
palette := make(color.Palette, n)
triplets := make([]byte, n*3)
if _, err := io.ReadFull(r, triplets); err != nil {
return int(i), nil, err
}
for j := 0; j < int(n); j++ {
o := j * 3
palette[j] = color.RGBA{
R: triplets[o+0],
G: triplets[o+1],
B: triplets[o+2],
A: 0xff,
index = int(header.Index)
palette = make(color.Palette, header.Colors)
for i := 0; i < int(header.Colors); i++ {
if palette[i], err = readRGB24(r); err != nil {
return
}
}
return int(i), palette, nil
return
}
func (f *Frame) readVNIData(r *bufio.Reader, version int) (err error) {
func (f *Frame) readVNIData(r io.Reader, version int) (err error) {
var (
planeSize int16
delay uint16
@ -176,58 +161,93 @@ func (f *Frame) readVNIData(r *bufio.Reader, version int) (err error) {
return
}
f.Delay = time.Duration(delay) * time.Millisecond
log.Printf("frame: %d planes, delay %s", planeSize, f.Delay)
if version >= 4 {
f.Hash = make([]byte, 4)
if _, err = io.ReadFull(r, f.Hash); err != nil {
if f.Checksum, err = readUint32(r); err != nil {
return
}
f.Checksum = binary.BigEndian.Uint32(f.Hash)
log.Printf("frame checksum: %#08x", f.Checksum)
}
if f.Bits, err = r.ReadByte(); err != nil {
var bits uint8
if bits, err = readByte(r); err != nil {
return
}
var isCompressed bool
if version < 3 {
return f.readVNIPlanes(r, int(planeSize))
log.Printf("frame: %dx%d byte plane data", bits, planeSize)
return f.readVNIPlanes(r, int(planeSize), int(bits))
} else if isCompressed, err = readBool(r); err != nil {
return
} else if !isCompressed {
log.Printf("frame: %dx%d byte plane data, not compressed", bits, planeSize)
return f.readVNIPlanes(r, int(planeSize), int(bits))
}
return f.readVNIPlanesCompressed(r, int(planeSize))
log.Printf("frame: %dx%d byte plane data, compressed", bits, planeSize)
var compressedSize int32
if err = readBE(r, &compressedSize); err != nil {
return
}
var compressedPlanes = make([]byte, compressedSize)
if _, err = io.ReadFull(r, compressedPlanes); err != nil {
return
}
var decompressedPlanes []byte
if decompressedPlanes, err = heatshrink.Decompress(10, 0, compressedPlanes); err != nil {
return
}
log.Printf("decompressedPlanes %d bytes to %d", compressedSize, len(decompressedPlanes))
return f.readVNIPlanes(bufio.NewReader(bytes.NewReader(decompressedPlanes)), int(planeSize), int(bits))
}
func (f *Frame) readVNIPlanes(r *bufio.Reader, n int) (err error) {
for i := 0; i < int(f.Bits); i++ {
func (f *Frame) readVNIPlanes(r io.Reader, planeSize, bitLength int) (err error) {
h := bytes.NewBuffer(nil)
b := bufio.NewReader(io.TeeReader(r, h))
for i := 0; i < bitLength; i++ {
var marker byte
if marker, err = r.ReadByte(); err != nil {
if marker, err = b.ReadByte(); err != nil {
return
}
if marker == 0x6d {
f.Mask = make([]byte, n)
if _, err = io.ReadFull(r, f.Mask); err != nil {
f.Mask = make([]byte, planeSize)
if _, err = io.ReadFull(b, f.Mask); err != nil {
return
}
} else {
p := &Plane{
Marker: marker,
}
if err = p.readVNIData(r, n, marker); err != nil {
if err = p.readVNIData(b, planeSize, marker); err != nil {
return
}
f.Plane = append(f.Plane, p)
}
}
return
}
func (f *Frame) readVNIPlanesCompressed(r *bufio.Reader, n int) (err error) {
var compressed byte
if compressed, err = r.ReadByte(); err != nil {
return
} else if compressed == 0 {
return f.readVNIPlanes(r, n)
// Correct checksum.
if f.Checksum == 0 {
f.Checksum = checkSum(h.Bytes())
}
// Decode planes to raw bitmap data.
for i, p := range f.Plane {
for j, c := range p.Data {
c = reverseBits(c)
for k := 0; k < 8; k++ {
if c&1<<k != 0 {
f.Pix[j*8+k] |= 1 << i
}
}
}
}
return ErrHeatShrink
return
}
func (p *Plane) readVNIData(r *bufio.Reader, n int, marker byte) (err error) {
p.Marker = marker
p.Data = make([]byte, n)
@ -236,3 +256,5 @@ func (p *Plane) readVNIData(r *bufio.Reader, n int, marker byte) (err error) {
}
return
}
*/

52
animationset.go

@ -0,0 +1,52 @@
package dmd
import (
"errors"
"fmt"
"io"
"strings"
)
var (
ErrHeaderMagic = errors.New("dmd: invalid header magic")
)
type animationSet interface {
DecodeAnimationSet(r io.Reader) ([]*Animation, error)
}
func ReadAnimations(reader io.ReadSeeker) ([]*Animation, error) {
if err := rewind(reader); err != nil {
return nil, err
}
var magic [4]byte
if _, err := io.ReadFull(reader, magic[:]); err != nil {
return nil, err
}
var (
id = cString(magic[:])
set animationSet
)
switch {
case id == "ANIM":
set = vniAnimationSet{} // pin2DMDAnimationSet{}
case id == "VPIN":
set = vniAnimationSet{}
case strings.HasPrefix(id, runDMDHeaderMagic):
return readRunDMDAnimations(reader)
case id == "\x11HDD":
return nil, errors.New("dmd: please unpack the HDD Raw Copy image to raw (dd) format")
default:
return nil, fmt.Errorf("dmd: magic %q does not denote a supported format", cString(magic[:]))
}
if _, err := reader.Seek(0, io.SeekStart); err != nil {
return nil, err
}
return set.DecodeAnimationSet(reader)
}

148
animationset_pin2dmd.go

@ -0,0 +1,148 @@
package dmd
import (
"fmt"
"image/color"
"io"
"log"
)
type pin2DMDAnimationSet struct{}
func (s pin2DMDAnimationSet) DecodeAnimationSet(r io.Reader) (set []*Animation, err error) {
var header [4]byte
if _, err = io.ReadFull(r, header[:]); err != nil {
return
} else if string(header[:]) != "ANIM" {
return nil, ErrHeaderMagic
}
var version uint16
if version, err = readUint16(r); err != nil {
return
}
var animations uint16
if animations, err = readUint16(r); err != nil {
return
}
if version > 6 {
return nil, fmt.Errorf("dmd: pin2dmd animation version %d is not supported", version)
}
if version >= 4 {
log.Printf("pin2dmd: skipping %d bytes of animation indexes", 4*int(animations))
for i := 0; i < int(animations); i++ {
if _, err = readUint32(r); err != nil {
return
}
}
}
set = make([]*Animation, animations)
log.Printf("vni: reading %d animations from %s v%d", animations, string(header[:]), version)
for i := 0; i < int(animations); i++ {
if set[i], err = s.decodeAnimation(r, version); err != nil {
return
}
}
return set, nil
}
func (s pin2DMDAnimationSet) decodeAnimation(r io.Reader, version uint16) (a *Animation, err error) {
a = &Animation{
Width: 128,
Height: 32,
PaletteIndex: -1,
}
if a.Name, err = readString(r); err != nil {
return
}
var header struct {
Cycles uint16
Hold uint16
ClockFrom uint16
ClockSmall uint8
ClockFront uint8
ClockXOffset uint16
ClockYOffset uint16
Delay uint16
_ uint8
FSK uint8
Frames int16
}
if err = readBE(r, &header); err != nil {
return
}
frames := int(header.Frames)
if frames < 0 {
frames += 65536
}
if version >= 2 {
// Custom palette.
var (
index uint8
palette color.Palette
)
if index, palette, err = readPin2DMDPalette(r); err != nil {
return nil, fmt.Errorf("error decoding palette: %w", err)
}
//if a.Name == "Tilt" {
log.Printf("custom palette %d in %q: %d colors: %+v", index, a.Name, len(palette), palette)
//}
a.Palettes[int(index)] = palette
}
if version >= 3 {
// Edit mode.
if _, err = readByte(r); err != nil {
return
}
}
if version >= 4 {
// Size of the panel.
if a.Width, err = readInt16Int(r); err != nil {
return
}
if a.Height, err = readInt16Int(r); err != nil {
return
}
} else {
a.Width = pin2DMDPanelWidth
a.Height = pin2DMDPanelHeight
}
if version >= 5 {
// Masks.
var n int16
if err = readBE(r, &n); err != nil {
return
}
}
if version >= 6 {
var isRecorded bool
if isRecorded, err = readBool(r); err != nil {
return
}
if isRecorded {
// Link to the recording (ignored).
if _, err = readString(r); err != nil {
return
}
// Start frame (ignored)
var n int16
if err = readBE(r, &n); err != nil {
return
}
}
}
return
}

281
animationset_vni.go

@ -0,0 +1,281 @@
package dmd
import (
"bytes"
"image"
"image/color"
"io"
"log"
"time"
"maze.io/x/dmd/internal/heatshrink"
)
const vniDelayStep = time.Millisecond
type vniAnimationSet struct{}
func (s vniAnimationSet) DecodeAnimationSet(r io.Reader) (set []*Animation, err error) {
var header [4]byte
if _, err = io.ReadFull(r, header[:]); err != nil {
return
} else if string(header[:]) != "VPIN" && string(header[:]) != "ANIM" {
return nil, ErrHeaderMagic
}
var version int16
if version, err = readInt16(r); err != nil {
return
}
var animations int16
if animations, err = readInt16(r); err != nil {
return
}
if version >= 2 {
log.Printf("vni: skipping %d bytes of animation indexes", 4*int(animations))
for i := 0; i < int(animations); i++ {
if _, err = readUint32(r); err != nil {
return
}
}
}
set = make([]*Animation, animations)
log.Printf("vni: reading %d animations from %s v%d", animations, string(header[:]), version)
for i := 0; i < int(animations); i++ {
if set[i], err = s.decodeAnimation(r, version); err != nil {
return
}
}
return set, nil
}
func (s vniAnimationSet) decodeAnimation(r io.Reader, version int16) (a *Animation, err error) {
a = &Animation{
Width: 128,
Height: 32,
PaletteIndex: -1,
}
if a.Name, err = readString(r); err != nil {
return
}
var (
clockOffsetX int16
clockOffsetY int16
frameCount int16
)
if _, err = readInt16(r); err != nil { // cycles
return
}
if _, err = readInt16(r); err != nil { // hold
return
}
if _, err = readInt16(r); err != nil { // clock from
return
}
if a.ClockSmall, err = readBool(r); err != nil {
return
}
if a.ClockInFront, err = readBool(r); err != nil {
return
}
if clockOffsetX, err = readInt16(r); err != nil {
return
}
if clockOffsetY, err = readInt16(r); err != nil {
return
}
a.ClockOffset = image.Pt(int(clockOffsetX), int(clockOffsetY))
if _, err = readInt16(r); err != nil { // refresh delay
return
}
if _, err = readByte(r); err != nil { // type
return
}
if _, err = readByte(r); err != nil { // fsk
return
}
if frameCount, err = readInt16(r); err != nil {
return
}
frames := int(frameCount)
if frames < 0 {
frames += 65536
}
if version >= 2 {
if err = s.readPalettesAndColors(r, a); err != nil {
return
}
}
if version >= 3 {
if _, err = readByte(r); err != nil { // edit mode
return
}
}
if version >= 4 {
if a.Width, err = readInt16Int(r); err != nil {
return
}
if a.Height, err = readInt16Int(r); err != nil {
return
}
}
if version >= 5 {
if err = s.readMasks(r, a); err != nil {
return
}
}
log.Printf("vni: reading %d frames for animation %q", frames, a.Name)
a.Index = make([]int, frames)
a.Frame = make([]*Frame, frames)
for i := 0; i < frames; i++ {
a.Index[i] = i
if a.Frame[i], err = s.readFrame(r, a.Width, a.Height, version); err != nil {
return
}
}
return
}
func (s vniAnimationSet) readPalettesAndColors(r io.Reader, a *Animation) (err error) {
if a.PaletteIndex, err = readInt16Int(r); err != nil {
a.PaletteIndex = -1
return err
}
var colors int
if colors, err = readInt16Int(r); err != nil {
return
}
if colors <= 0 {
log.Printf("vni: no colors defined, palette index %d", a.PaletteIndex)
return
}
a.Palette = make(color.Palette, colors)
for i := 0; i < colors; i++ {
if a.Palette[i], err = readRGB24(r); err != nil {
return
}
}
log.Printf("vni: found %d colors for palette %d", colors, a.PaletteIndex)
return
}
func (s vniAnimationSet) readMasks(r io.Reader, a *Animation) (err error) {
var masks int
if masks, err = readInt16Int(r); err != nil {
return
}
a.Masks = make([][]byte, masks)
for i := 0; i < masks; i++ {
if _, err = readByte(r); err != nil { // locked
return
}
var size int16
if size, err = readInt16(r); err != nil {
return
}
a.Masks[i] = make([]byte, size)
if _, err = io.ReadFull(r, a.Masks[i]); err != nil {
return
}
}
return
}
func (s vniAnimationSet) readFrame(r io.Reader, width, height int, version int16) (f *Frame, err error) {
f = NewFrame(width, height)
var (
planeSize int16
delay uint16
bitLength byte
)
if planeSize, err = readInt16(r); err != nil {
return
}
if delay, err = readUint16(r); err != nil {
return
}
f.Delay = time.Duration(delay) * vniDelayStep
if version >= 4 {
if f.Checksum, err = readUint32(r); err != nil {
return
}
}
if bitLength, err = readByte(r); err != nil {
return
}
f.Plane = make([]*Plane, 0, bitLength)
if version < 3 {
return f, s.readPlanes(r, f, bitLength, planeSize)
}
var isCompressed bool
if isCompressed, err = readBool(r); err != nil {
return
} else if isCompressed {
var (
size int32
compressedPlanes []byte
decompressedPlanes []byte
)
if size, err = readInt32(r); err != nil {
return
}
compressedPlanes = make([]byte, size)
if decompressedPlanes, err = heatshrink.Decompress(10, 0, compressedPlanes); err != nil {
return
}
return f, s.readPlanes(bytes.NewReader(decompressedPlanes), f, bitLength, planeSize)
}
return f, s.readPlanes(r, f, bitLength, planeSize)
}
func (s vniAnimationSet) readPlanes(r io.Reader, f *Frame, bitLength byte, planeSize int16) (err error) {
for i := 0; i < int(bitLength); i++ {
var marker byte
if marker, err = readByte(r); err != nil {
return
}
if marker == 0x6d {
f.Mask = make([]byte, planeSize)
if _, err = io.ReadFull(r, f.Mask); err != nil {
return
}
} else {
var p *Plane
if p, err = s.readPlane(r, marker, planeSize); err != nil {
return
}
f.Plane = append(f.Plane, p)
}
}
return
}
func (s vniAnimationSet) readPlane(r io.Reader, marker byte, planeSize int16) (p *Plane, err error) {
p = &Plane{
Marker: marker,
Data: make([]byte, planeSize),
}
if _, err = io.ReadFull(r, p.Data); err != nil {
return
}
return
}

67
animationset_vni_test.go

@ -0,0 +1,67 @@
package dmd
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestReadVNIAnimations(t *testing.T) {
tests := []struct {
Name string
Palette string
}{
{
Name: "sprk_103.vni",
Palette: "sprk_103.pal",
},
{
Name: "mb_106b.vni",
Palette: "mb_106b.pal",
},
{
Name: "ID4_1.00.vni",
Palette: "ID4_1.00.pal",
},
}
for _, test := range tests {
t.Run(test.Name, func(it *testing.T) {
f, err := os.Open(filepath.Join("testdata", test.Name))
if err != nil {
it.Skip(err)
}
defer f.Close()
all, err := ReadAnimations(f)
if err != nil {
it.Fatal(err)
}
it.Logf("%s: %d animations", test.Name, len(all))
var c *Coloring
if test.Palette != "" {
if c, err = LoadColoring(filepath.Join("testdata", test.Palette)); err != nil {
it.Fatal(err)
}
it.Logf("%s: coloring: %d palettes, %d mappings, %d masks", test.Palette, len(c.Palettes), len(c.Mappings), len(c.Masks))
}
for i, a := range all {
//if len(a.Frame) > 1 {
if strings.Contains(a.Name, "game over") {
it.Logf("%s: animation %d: %d frames, duration %s", test.Name, i, len(a.Frame), a.Duration())
it.Logf("%s: animation: %#+v", test.Name, a)
/*
for _, f := range a.Frame {
for _, p := range f.Plane {
dumpPlane(p.Data, f.Width/8)
}
}
*/
testSaveGIF(t, test.Name, a, c)
}
}
})
}
}

110
checksum.go

@ -0,0 +1,110 @@
package dmd
func checkSum(b []byte, reverse bool) (cs uint32) {
cs = ^cs
if reverse {
for _, c := range b {
cs = (cs >> 8) ^ checksumTable[byte(cs)^reverseTable[c]]
}
} else {
for _, c := range b {
cs = (cs >> 8) ^ checksumTable[byte(cs)^c]
}
}
return ^cs
}
func checkSumWithMask(b, m []byte, reverse bool) (cs uint32) {
cs = ^cs
if reverse {
for i, c := range b {
cs = (cs >> 8) ^ checksumTable[byte(cs)^reverseTable[c&m[i]]]
}
} else {
for i, c := range b {
cs = (cs >> 8) ^ checksumTable[byte(cs)^(c&m[i])]
}
}
return ^cs
}
var checksumTable = []uint32{
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F,
0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2,
0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9,
0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172,
0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C,
0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423,
0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924,
0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106,
0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D,
0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E,
0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950,
0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7,
0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0,
0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA,
0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81,
0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A,
0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84,
0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB,
0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC,
0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E,
0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55,
0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236,
0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28,
0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F,
0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38,
0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242,
0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69,
0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2,
0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC,
0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693,
0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D,
}
var reverseTable = []byte{
0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0,
0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0,
0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8,
0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8,
0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4,
0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4,
0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec,
0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc,
0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2,
0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2,
0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea,
0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa,
0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6,
0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6,
0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee,
0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe,
0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1,
0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1,
0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9,
0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9,
0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5,
0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5,
0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed,
0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd,
0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3,
0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3,
0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb,
0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb,
0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7,
0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7,
0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef,
0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff,
}

50
cmd/dmd-pal/main.go

@ -0,0 +1,50 @@
package main
import (
"flag"
"fmt"
"os"
"sort"
"maze.io/x/dmd"
)
func main() {
flag.Parse()
for _, name := range flag.Args() {
dump(name)
}
}
func dump(name string) {
fmt.Println("reading", name)
coloring, err := dmd.LoadColoring(name)
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
return
}
checksums := make([]uint32, 0, len(coloring.Mappings))
for checksum := range coloring.Mappings {
checksums = append(checksums, checksum)
}
sort.Slice(checksums, func(i, j int) bool { return checksums[i] < checksums[j] })
for i, palette := range coloring.Palettes {
fmt.Println("palette", i+1)
for j, color := range palette.Colors {
// ESC[ 38;2;⟨r⟩;⟨g⟩;⟨b⟩ m Select RGB foreground color
r, g, b, _ := color.RGBA()
fmt.Printf("%02d:\x1b[38;2;%d;%d;%dm▂▄▆█\x1b[0m ", j+1, (r>>8)&0xff, (g>>8)&0xff, (b>>8)&0xff)
<