Browse Source

Refactoring

master
maze 1 year ago
parent
commit
e8e55ce73f
44 changed files with 1584 additions and 2408 deletions
  1. +168
    -88
      animation.go
  2. +0
    -341
      animation_pin2dmd.go
  3. +0
    -156
      animation_rundmd.go
  4. +0
    -153
      animation_test.go
  5. +0
    -260
      animation_vni.go
  6. +0
    -52
      animationset.go
  7. +0
    -148
      animationset_pin2dmd.go
  8. +0
    -281
      animationset_vni.go
  9. +0
    -67
      animationset_vni_test.go
  10. +2
    -2
      checksum.go
  11. +1
    -1
      cmd/dmd-pal/main.go
  12. +2
    -2
      cmd/dmd2gif/main.go
  13. +260
    -0
      color.go
  14. +2
    -2
      color_test.go
  15. +0
    -373
      colorize.go
  16. +0
    -65
      display.go
  17. +250
    -0
      dmd1.go
  18. +88
    -0
      dmd1_test.go
  19. +0
    -107
      frame.go
  20. +202
    -0
      image.go
  21. +0
    -160
      palette_pin2dmd.go
  22. +132
    -0
      rundmd.go
  23. +70
    -0
      rundmd_test.go
  24. BIN
      testdata/dmd1/1942.dmd
  25. BIN
      testdata/dmd1/1942.gif
  26. BIN
      testdata/dmd1/afterburner.dmd
  27. BIN
      testdata/dmd1/afterburner.gif
  28. +0
    -0
      testdata/vpin/ID4_1.00.pal
  29. +0
    -0
      testdata/vpin/ID4_1.00.vni
  30. +0
    -0
      testdata/vpin/TFTC_ColorDMD_1.0.pal
  31. +0
    -0
      testdata/vpin/TFTC_ColorDMD_1.0.vni
  32. BIN
      testdata/vpin/mb_106b.pal
  33. BIN
      testdata/vpin/mb_106b.vni
  34. BIN
      testdata/vpin/smb.ani
  35. BIN
      testdata/vpin/smb.fsq
  36. BIN
      testdata/vpin/smb.pal
  37. +0
    -0
      testdata/vpin/sprk_103.pal
  38. +0
    -0
      testdata/vpin/sprk_103.vni
  39. BIN
      testdata/vpin/sttng.ani
  40. BIN
      testdata/vpin/sttng.pal
  41. BIN
      testdata/vpin/sttng.vni
  42. +1
    -150
      util.go
  43. +315
    -0
      vpin.go
  44. +91
    -0
      vpin_test.go

+ 168
- 88
animation.go View File

@ -2,130 +2,210 @@ package dmd
import (
"errors"
"fmt"
"image"
"image/color"
"image/gif"
"log"
"io"
"time"
)
var ErrHeatShrink = errors.New("dmd: heat shrink compression is not supported")
var (
ErrHeaderMagic = errors.New("dmd: unexpected header magic")
ErrSeek = errors.New("dmd: illegal seek offset")
)
type Animation struct {
type Animation interface {
// Name of the animation.
Name string
Name() string
// IsMask indicates the animation contains masks.
IsMask() bool
// size of the animation.
Size() (width, height int)
// Width of the animation in pixels.
Width int
// ClockInFront checks if the clock should be over the image.
ClockInFront() bool
// Height of the animation in pixels.
Height int
// ClockOffset is the offset of the clock.
ClockOffset() image.Point
// Palette is a custom palette.
Palette color.Palette
// Duration of the animation.
Duration() time.Duration
// PaletteIndex is the index of the custom palette.
PaletteIndex int
// Len is the length of the animation in number of frames.
Len() int
// Index of each frame, used in sequenced animations. If set to nil,
// it is assumed this is a linear animation.
Index []int
// Next frame, will return EOF when end of animation is reached.
NextFrame() (image.Image, time.Duration, error)
// Frame data.
Frame []*Frame
// Seek to frame.
SeekFrame(pos int) error
}
// Masks data.
Masks [][]byte
// Animations is 0 or more Animation.
type Animations []Animation
// Palettes data.
Palettes map[int]color.Palette // Frame palette (if any).
// Namer can provide a name.
type Namer interface {
Name() string
}
// ClockOffset location data.
ClockOffset image.Point
// ImageAnimation consists of (bitmap) images.
type imageAnimation struct {
name string
width, height int
isMask bool
clockInFront bool
clockOffset image.Point
delay []time.Duration
index []int
frame []image.Image
pos int
palette color.Palette
}
// ClockInFront renders the clock in front of the frame.
ClockInFront bool
func (a *imageAnimation) Name() string { return a.name }
// ClockSmall displays a small clock for a given index.
ClockSmall bool
func (a *imageAnimation) Size() (width, height int) {
return a.width, a.height
}
func (a Animation) Duration() time.Duration {
var sum time.Duration
if a.Index == nil {
func (a *imageAnimation) ClockInFront() bool { return a.clockInFront }
func (a *imageAnimation) ClockOffset() image.Point { return a.clockOffset }
func (a *imageAnimation) IsMask() bool { return a.isMask }
func (a *imageAnimation) Duration() (total time.Duration) {
if len(a.index) == 0 {
// Linear.
for _, f := range a.Frame {
sum += f.Delay
for _, delay := range a.delay {
total += delay
}
} else {
// Sequenced.
for _, i := range a.Index {
sum += a.Frame[i].Delay
for _, index := range a.index {
if index >= 0 {
total += a.delay[index]
}
}
}
return sum
return
}
func (a *Animation) GIF(palette color.Palette, scale uint) *gif.GIF {
return a.generateGIF(nil, palette, scale)
func (a *imageAnimation) Len() int {
if l := len(a.index); l > 0 {
// Sequenced.
return l
}
// Linear.
return len(a.frame)
}
func (a *Animation) ColoredGIF(coloring *Coloring, scale uint) *gif.GIF {
return a.generateGIF(coloring, nil, scale)
// Next frame, will return EOF when end of animation is reached.
func (a *imageAnimation) NextFrame() (image.Image, time.Duration, error) {
pos := a.pos
if len(a.index) > 0 {
if pos > len(a.index) || pos > len(a.delay) {
return nil, 0, io.EOF
}
a.pos++
// Sequenced.
index := a.index[pos]
if index < 0 {
return newTransparentFrame(a.width, a.height), a.delay[pos], nil
}
return a.frame[index], a.delay[pos], nil
}
// Linear.
if pos > len(a.frame) || pos > len(a.delay) {
return nil, 0, io.EOF
}
a.pos++
return a.frame[pos], a.delay[pos], nil
}
func (a *Animation) generateGIF(coloring *Coloring, overridePalette color.Palette, scale uint) *gif.GIF {
if scale <= 0 {
scale = 1
func newTransparentFrame(width, height int) image.Image {
return &image.Paletted{
Pix: make([]byte, width*height),
Stride: width,
Rect: image.Rect(0, 0, width, height),
Palette: color.Palette{color.Transparent},
}
}
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 {
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
}
}
log.Printf("palette: %#+v", p)
// Seek to frame.
func (a *imageAnimation) SeekFrame(pos int) error {
var l int
if l = len(a.index); l == 0 {
l = len(a.frame)
}
if pos < 0 || pos >= l {
return ErrSeek
}
a.pos = pos
return nil
}
for j, i := range a.Index {
if i < 0 {
continue
}
f := a.Frame[i]
if f == nil {
panic(fmt.Sprintf("index %d points to %d which has a nil frame!", i, j))
}
g.Delay = append(g.Delay, int(f.Delay/time.Millisecond/10))
g.Image = append(g.Image, toPaletted(Panel{
PalettedImage: &FrameImage{
Frame: f,
Palette: p,
},
Dot: int(scale),
}, p))
type colorizedAnimation struct {
Animation
palette color.Palette
}
func (a colorizedAnimation) NextFrame() (image.Image, time.Duration, error) {
i, t, err := a.Animation.NextFrame()
i = a.colorize(i)
return i, t, err
}
func (a colorizedAnimation) colorize(i image.Image) image.Image {
if i == nil {
return nil
}
if p, ok := i.(*image.Paletted); ok {
// log.Printf("colorize %T with %d colors", i, len(p.Palette))
return &image.Paletted{
Pix: p.Pix,
Stride: p.Stride,
Rect: p.Rect,
Palette: a.palette,
}
}
return i
}
return g
func Colorize(a Animation, p color.Palette) Animation {
return colorizedAnimation{
Animation: a,
palette: p,
}
}
// Default palettes.
var (
Mask = color.Palette{
color.Alpha{A: 0x00}, color.Alpha{A: 0x11}, color.Alpha{A: 0x22}, color.Alpha{A: 0x33},
color.Alpha{A: 0x44}, color.Alpha{A: 0x55}, color.Alpha{A: 0x66}, color.Alpha{A: 0x77},
color.Alpha{A: 0x88}, color.Alpha{A: 0x99}, color.Alpha{A: 0xaa}, color.Alpha{A: 0xbb},
color.Alpha{A: 0xcc}, color.Alpha{A: 0xdd}, color.Alpha{A: 0xee}, color.Alpha{A: 0xff},
}
Gray = color.Palette{
color.Gray{Y: 0x00}, color.Gray{Y: 0x11}, color.Gray{Y: 0x22}, color.Gray{Y: 0x33},
color.Gray{Y: 0x44}, color.Gray{Y: 0x55}, color.Gray{Y: 0x66}, color.Gray{Y: 0x77},
color.Gray{Y: 0x88}, color.Gray{Y: 0x99}, color.Gray{Y: 0xaa}, color.Gray{Y: 0xbb},
color.Gray{Y: 0xcc}, color.Gray{Y: 0xdd}, color.Gray{Y: 0xee}, color.Gray{Y: 0xff},
}
Amber = color.Palette{
color.RGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff}, color.RGBA{R: 0x11, G: 0x08, B: 0x00, A: 0xff},
color.RGBA{R: 0x22, G: 0x11, B: 0x00, A: 0xff}, color.RGBA{R: 0x33, G: 0x19, B: 0x00, A: 0xff},
color.RGBA{R: 0x44, G: 0x22, B: 0x00, A: 0xff}, color.RGBA{R: 0x55, G: 0x2a, B: 0x00, A: 0xff},
color.RGBA{R: 0x66, G: 0x33, B: 0x00, A: 0xff}, color.RGBA{R: 0x77, G: 0x3b, B: 0x00, A: 0xff},
color.RGBA{R: 0x88, G: 0x44, B: 0x00, A: 0xff}, color.RGBA{R: 0x99, G: 0x4c, B: 0x00, A: 0xff},
color.RGBA{R: 0xaa, G: 0x55, B: 0x00, A: 0xff}, color.RGBA{R: 0xbb, G: 0x5d, B: 0x00, A: 0xff},
color.RGBA{R: 0xcc, G: 0x66, B: 0x00, A: 0xff}, color.RGBA{R: 0xdd, G: 0x6e, B: 0x00, A: 0xff},
color.RGBA{R: 0xee, G: 0x77, B: 0x00, A: 0xff}, color.RGBA{R: 0xff, G: 0x7f, B: 0x00, A: 0xff},
}
)

+ 0
- 341
animation_pin2dmd.go View File

@ -1,341 +0,0 @@
package dmd
import (
"bufio"
"bytes"
"encoding/binary"
"fmt"
"image/color"
"io"
"io/ioutil"
"log"
"time"
"maze.io/x/dmd/internal/heatshrink"
)
const (
pin2DMDPanelWidth = 128
pin2DMDPanelHeight = 32
)
/*
4 Byte Header: ANIM
2 Byte Version: 00 01
2 Byte Anzahl der Animationen: 00 02
N Byte Name der Animation in modified UTF-8
first 2 Byte gives length of string incl 2 byte length header
2 Byte: cycles
2 Byte: hold
2 Byte: clockFrom
1 Byte: clock small
1 Byte: clock in front
2 Byte: xoffset for small clock
2 Byte: yoffset for small clock
2 Byte: refresh delay in millis
1 Byte: Animation Type (not relevant)
1 Byte: fsk tag
2 Byte: frame set count
each frame consists of 2 or 3 planes and optionally a mask plane
each plane is tagged with its type 0,1,2 for normal bitplanes in
grayscale animations or M ( for mask, the order is always m,0,1,2
where m and 2 are optional
foreach frame:
2 Byte: plane size in Byte. normally 512 byte
2 Byte: frame duration in ms
1 Byte: number of planes
foreach plane:
1 Byte: type of plane, m,0,1,2 see above
plane size Byte frame data: 1 Bit = 1 Pixel
Bit 7 is pixel on the left (BIG_ENDIAN)
end
end
*/
type pin2DMDHeader struct {
Version uint16
Animations uint16
}
func readPin2DMDAnimations(rs io.ReadSeeker) (all []*Animation, err error) {
var h pin2DMDHeader
if err = readBE(rs, &h); err != nil {
return
}
if h.Version > 6 {
return nil, fmt.Errorf("dmd: pin2dmd animation version %d is not supported", h.Version)
}
if h.Version >= 2 {
// Skip over indexes.
if _, err = ioutil.ReadAll(io.LimitReader(rs, 4*int64(h.Animations))); err != nil {
return
}
}
r := bufferedReader(rs)
for i := 0; i < int(h.Animations); i++ {
a := &Animation{
Palettes: make(map[int]color.Palette),
}
if err = a.readPin2DMDData(r, int(h.Version)); err != nil {
return
}
all = append(all, a)
}
return
}
type pin2DMDAnimationHeader struct {
Cycles uint16
Hold uint16
ClockFrom uint16
ClockSmall uint8
ClockFront uint8
ClockXOffset uint16
ClockYOffset uint16
Delay uint16
_ uint8
FSK uint8
Frames int16
}
func (a *Animation) readPin2DMDData(r *bufio.Reader, version int) (err error) {
var nameLength int16
if err = readBE(r, &nameLength); err != nil {
return
}
if nameLength > 0 {
name := make([]byte, nameLength)
if _, err = io.ReadFull(r, name); err != nil {
return nil
}
a.Name = cString(name)
}
var h pin2DMDAnimationHeader
if err = readBE(r, &h); err != nil {
return
}
// log.Printf("pin2dmd header for %q: %#+v", a.Name, h)
frames := int(h.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 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 = r.ReadByte(); err != nil {
return
}
}
if version >= 4 {
// Size of the panel.
var w, h int16
if err = readBE(r, &w); err != nil {
return
}
if err = readBE(r, &h); err != nil {
return
}
a.Width = int(w)
a.Height = int(h)
} 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 recorded uint8
if recorded, err = r.ReadByte(); err != nil {
return
}
if recorded != 0 {
// 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 a.readPin2DMDFrames(r, int(h.Frames), version)
}
type pin2DMDPaletteHeader struct {
Index int16
Colors int16
}
func readPin2DMDPalette(r io.Reader) (index uint8, palette color.Palette, err error) {
var h pin2DMDPaletteHeader
if err = readBE(r, &h); err != nil {
return
}
index = uint8(h.Index)
log.Printf("read %d color palette for index %d", h.Colors, h.Index)
if h.Colors == 0 && index < 16 {
return index, defaultPalettes[index], nil
}
triplets := make([]byte, h.Colors*3)
if _, err = io.ReadFull(r, triplets); err != nil {
return
}
palette = make(color.Palette, h.Colors)
for i := 0; i < int(h.Colors); i++ {
o := i * 3
palette[i] = color.RGBA{
R: triplets[o+0],
G: triplets[o+1],
B: triplets[o+2],
A: 0xff,
}
}
return
}
func (a *Animation) readPin2DMDFrames(r *bufio.Reader, n, version int) (err error) {
a.Index = make([]int, n)
a.Frame = make([]*Frame, n)
for i := 0; i < n; i++ {
// log.Printf("pin2dmd: frame %d/%d", i+1, n)
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)
}
}
return
}
type pin2DMDFrameHeader struct {
Size uint16
Delay uint16
}
func (f *Frame) readPin2DMDData(r *bufio.Reader, version int) (err error) {
var h pin2DMDFrameHeader
if err = readBE(r, &h); err != nil {
return
}
/*
if h.Size%32 != 0 {
return fmt.Errorf("dmd: odd pin2dmd frame size %d", h.Size)
}
*/
f.Delay = time.Duration(h.Delay) * time.Millisecond
// log.Println(f.Delay)
if version >= 4 {
var hash [4]byte
if _, err = io.ReadFull(r, hash[:]); err != nil {
return
}
f.Checksum = binary.BigEndian.Uint32(hash[:])
}
var nPlanes uint8
if nPlanes, err = r.ReadByte(); err != nil {
return
}
// log.Printf(" frame: version %d %#+v", version, h)
// log.Printf(" frame: %d planes of %db %#+v", nPlanes, h.Size, f)
pr := r
if version >= 3 {
var isCompressed bool
if isCompressed, err = readBool(r); err != nil {
return
} else if isCompressed {
var compressedSize int16
if err = readBE(r, &compressedSize); err != nil {
return
}
var compressed []byte
if compressed, err = ioutil.ReadAll(io.LimitReader(r, int64(compressedSize))); err != nil {
return
}
var decompressed []byte
if decompressed, err = heatshrink.Decompress(10, 5, compressed); err != nil {
return
}
log.Printf("decompressed %d bytes to %d", compressedSize, len(decompressed))
pr = bufio.NewReader(bytes.NewReader(decompressed))
}
}
if err = f.readPin2DMDPlanes(pr, int(nPlanes), int(h.Size)); err != nil {
return fmt.Errorf("error reading plane: %w", err)
}
return
}
func (f *Frame) readPin2DMDPlanes(r *bufio.Reader, nPlanes, size int) (err error) {
for i := 0; i < nPlanes; i++ {
var (
data = make([]byte, size)
marker byte
)
if marker, err = r.ReadByte(); err != nil {
return
}
if _, err = io.ReadFull(r, data); err != nil {
return fmt.Errorf("error reading plane %d/%d (%d bytes): %w", i+1, nPlanes, size, err)
}
if int(marker) < nPlanes {
f.Plane = append(f.Plane, &Plane{
Marker: marker,
Data: data,
})
} else {
f.Mask = data
}
}
return
}
func (p *Plane) readPin2DMDData(r io.Reader, size int) (err error) {
p.Data = make([]byte, size)
if _, err = io.ReadFull(r, p.Data); err != nil {
return
}
reverseBytes(p.Data, p.Data)
return
}

+ 0
- 156
animation_rundmd.go View File

@ -1,156 +0,0 @@
package dmd
import (
"io"
"log"
"time"
)
const (
runDMDPanelWidth = 128
runDMDPanelHeight = 32
runDMDHeaderMagic = "DGD"
runDMDHeaderOffset = 0xc800
runDMDHeaderBuildOffset = 0x01ef
runDMDHeaderSize = 0x0200
)
func readRunDMDAnimations(rs io.ReadSeeker) (all []*Animation, err error) {
if _, err = rs.Seek(3, io.SeekStart); err != nil {
return
}
var n uint16
if err = readBE(rs, &n); err != nil {
return
}
for i := 0; i < int(n); i++ {
a := new(Animation)
if err = a.readRunDMDData(rs, i); err != nil {
return
}
all = append(all, a)
}
return
}
type runDMDAnimationHeader struct {
ID uint16
_ uint8
Frames uint8
ByteOffset uint32
Entries uint8
Width uint8
Height uint8
_ [9]byte
Name [32]byte
}
type runDMDAnimationFrameHeader struct {
ID uint8
Delay uint8
}
func (a *Animation) readRunDMDData(rs io.ReadSeeker, n int) (err error) {
if _, err = rs.Seek(runDMDHeaderOffset+runDMDHeaderSize*int64(n), io.SeekStart); err != nil {
return
}
var h runDMDAnimationHeader
if err = readBE(rs, &h); err != nil {
return
}
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
}
var delays []time.Duration
for i := 0; i < int(h.Frames); i++ {
var fh runDMDAnimationFrameHeader
if err = readBE(rs, &fh); err != nil {
return
}
delays = append(delays, time.Duration(fh.Delay)*time.Millisecond)
a.Index = append(a.Index, int(fh.ID)-1)
}
if _, err = rs.Seek(runDMDHeaderOffset+int64(h.ByteOffset), io.SeekStart); err != nil {
return
}
b := make([]byte, a.Width*a.Height/2)
for i := 0; i < int(h.Frames); i++ {
o := runDMDHeaderSize + int64(h.ByteOffset)*runDMDHeaderSize
o += int64(a.Width) * int64(a.Height) * int64(i) / 2
if _, err = rs.Seek(o, io.SeekStart); err != nil {
return
}
f := NewFrame(a.Width, a.Height)
f.Delay = delays[i]
// Read interleaved (4-bits) planes.
if _, err = io.ReadFull(rs, b); err != nil {
return
}
// 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 {
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
}
// 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
}

+ 0
- 153
animation_test.go View File

@ -1,153 +0,0 @@
package dmd
import (
"fmt"
"image/gif"
"os"
"path/filepath"
"strings"
"testing"
)
func TestReadPin2DMDAnimations(t *testing.T) {
tests := []struct {
Name string
}{
{
Name: "smb.ani",
},
}
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))
/*
for i, a := range all {
it.Logf("%s: animation %d %q: %d frames, %d palettes, duration %s",
test.Name, i, a.Name, len(a.Frame), len(a.Palettes), a.Duration())
}
*/
for i, a := range all {
it.Logf("animation %d: %s", i, a.Name)
}
a := all[41]
it.Logf("animation %s", a.Name)
for i, f := range a.Frame {
it.Logf("frame %d:", i)
for j, p := range f.Plane {
it.Logf("plane %d: %#02x", j, p.Marker)
}
}
return
for _, a := range all {
it.Logf("%d palettes", len(a.Palettes))
for j, colors := range a.Palettes {
it.Logf("palette %d: %+v", j, colors)
}
if err := testSaveGIF(it, test.Name, a, nil); err != nil {
it.Errorf("%s: %v", a.Name, err)
}
/*
for i, frame := range a.Frame {
o, err := os.Create(filepath.Join(os.TempDir(), fmt.Sprintf("%s-%d.png", a.Name, i)))
if err != nil {
it.Fatal(err)
}
defer o.Close()
it.Logf("saving to %s", o.Name())
if err = png.Encode(o, Panel{
PalettedImage: &FrameImage{
Frame: frame,
Palette: Amber, // a.Palettes[3], //a.Palettes[uint8(i)],
},
Dot: 5,
}); err != nil {
it.Error(err)
}
it.Logf("frame %d [%02x]: %#+v", i, frame.Hash, frame)
for j, plane := range frame.Plane {
it.Logf("plane %d: %d (%#02x)", j, len(plane.Data)/128, plane.Marker)
// dumpPlane(plane.Data, 128/8)
}
}
*/
}
})
}
}
func testSaveGIF(t *testing.T, name string, a *Animation, c *Coloring) error {
t.Helper()
name = filepath.Base(name)
if ext := filepath.Ext(name); ext != "" {
name = name[:len(name)-len(ext)]
}
full := filepath.Join(os.TempDir(), fmt.Sprintf("dmd-%s-%s.gif", name, a.Name))
t.Logf("saving to %s", full)
o, err := os.Create(full)
if err != nil {
return err
}
if err := gif.EncodeAll(o, a.GIF(c, 5)); err != nil {
_ = o.Close()
_ = os.Remove(name)
return err
}
return o.Close()
}
func TestReadRunDMDAnimations(t *testing.T) {
name := os.Getenv("RUNDMD_IMAGE")
if name == "" {
t.Skip("environment variable RUNDMD_IMAGE not set")
}
f, err := os.Open(name)
if err != nil {
t.Fatal(err)
}
defer f.Close()
all, err := ReadAnimations(f)
if err != nil {
t.Fatal(err)
}
t.Logf("%s: %d animations", name, len(all))
}
func dumpPlane(plane []byte, bytesPerLine int) {
var sb strings.Builder
for i, l := 0, len(plane); i < l; i++ {
v := plane[i]
for j := 0; j < 8; j++ {
if ((128 >> j) & v) != 0x00 {
sb.WriteByte('#')
} else {
sb.WriteByte('.')
}
}
if i%bytesPerLine == bytesPerLine-1 {
fmt.Println(sb.String())
sb.Reset()
}
}
}

+ 0
- 260
animation_vni.go View File

@ -1,260 +0,0 @@
package dmd
/*
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 bool
ClockInFront bool
ClockOffsetX int16
ClockOffsetY int16
RefreshDelay int16
Type uint8
Fsk uint8
Frames int16
}
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 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
}
}
}
all = make([]*Animation, header.Animations)
for i := 0; i < int(header.Animations); i++ {
a := new(Animation)
if err = a.readVPIData(r, int(header.Version)); err != nil {
return
}
all = append(all, a)
}
return all, nil
}
func (a *Animation) readVPIData(r io.Reader, version int) (err error) {
if a.Name, err = readString(r); err != nil {
return
}
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 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
}
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 = readBool(r); err != nil {
return
}
}
if version >= 4 {
var size [2]int16
if err = readBE(r, &size); 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)
}
if version >= 5 {
// TODO: read masks
}
if version >= 6 {
// TODO: read links
}
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
}
type vpiPaletteHeader struct {
Index int16
Colors int16
}
func readVPIPalette(r io.Reader) (index int, palette color.Palette, err error) {
var header vpiPaletteHeader
if err = readBE(r, &header); err != nil {
return
}
if header.Colors <= 0 {
return -1, nil, nil
}
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
}
func (f *Frame) readVNIData(r io.Reader, version int) (err error) {
var (
planeSize int16
delay uint16
)
if err = readBE(r, &planeSize); err != nil {
return
}
if err = readBE(r, &delay); err != nil {
return
}
f.Delay = time.Duration(delay) * time.Millisecond
log.Printf("frame: %d planes, delay %s", planeSize, f.Delay)
if version >= 4 {
if f.Checksum, err = readUint32(r); err != nil {
return
}
log.Printf("frame checksum: %#08x", f.Checksum)
}
var bits uint8
if bits, err = readByte(r); err != nil {
return
}
var isCompressed bool
if version < 3 {
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))
}
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 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 = b.ReadByte(); err != nil {
return
}
if marker == 0x6d {
f.Mask = make([]byte, planeSize)
if _, err = io.ReadFull(b, f.Mask); err != nil {
return
}
} else {
p := &Plane{
Marker: marker,
}
if err = p.readVNIData(b, planeSize, marker); err != nil {
return
}
f.Plane = append(f.Plane, p)
}
}
// 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
}
func (p *Plane) readVNIData(r *bufio.Reader, n int, marker byte) (err error) {
p.Marker = marker
p.Data = make([]byte, n)
if _, err = io.ReadFull(r, p.Data); err != nil {
return
}
return
}
*/

+ 0
- 52
animationset.go View File

@ -1,52 +0,0 @@
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)
}

+ 0
- 148
animationset_pin2dmd.go View File

@ -1,148 +0,0 @@
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
}

+ 0
- 281
animationset_vni.go View File

@ -1,281 +0,0 @@
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