Browse Source

Initial import

master
maze 8 months ago
commit
e4b3fd5943
23 changed files with 2109 additions and 0 deletions
  1. +159
    -0
      animation.go
  2. +342
    -0
      animation_pin2dmd.go
  3. +114
    -0
      animation_rundmd.go
  4. +206
    -0
      animation_test.go
  5. +238
    -0
      animation_vni.go
  6. +65
    -0
      display.go
  7. +7
    -0
      doc.go
  8. +97
    -0
      frame.go
  9. +3
    -0
      go.mod
  10. +363
    -0
      internal/heatshrink/decoder.go
  11. +252
    -0
      palette.go
  12. +160
    -0
      palette_pin2dmd.go
  13. +31
    -0
      palette_test.go
  14. BIN
      testdata/ID4_1.00.pal
  15. BIN
      testdata/ID4_1.00.vni
  16. BIN
      testdata/TFTC_ColorDMD_1.0.pal
  17. BIN
      testdata/TFTC_ColorDMD_1.0.vni
  18. BIN
      testdata/smb.ani
  19. BIN
      testdata/smb.fsq
  20. BIN
      testdata/smb.pal
  21. BIN
      testdata/sprk_103.pal
  22. BIN
      testdata/sprk_103.vni
  23. +72
    -0
      util.go

+ 159
- 0
animation.go View File

@ -0,0 +1,159 @@
package dmd
import (
"errors"
"fmt"
"image"
"image/color"
"image/gif"
"io"
"log"
"strings"
"time"
)
var ErrHeatShrink = errors.New("dmd: heat shrink compression is not supported")
type Animation struct {
// Name of the animation.
Name string
// Width of the animation in pixels.
Width int
// Height of the animation in pixels.
Height int
// Palette is a custom palette.
Palette color.Palette
// PaletteIndex is the index of the custom palette.
PaletteIndex int
// Index of each frame, used in sequenced animations. If set to nil,
// it is assumed this is a linear animation.
Index []int
// Frame data.
Frame []*Frame
// Palettes data.
Palettes map[uint8]color.Palette // Frame palette (if any).
// Clock location data.
Clock []*image.Point
// ClockSmall displays a small clock for a given index.
ClockSmall []bool
}
func (a Animation) Duration() time.Duration {
var sum time.Duration
if a.Index == nil {
// Linear.
for _, f := range a.Frame {
sum += f.Delay
}
} else {
// Sequenced.
for _, i := range a.Index {
sum += a.Frame[i].Delay
}
}
return sum
}
func (a Animation) GIF(coloring *Coloring, scale uint) *gif.GIF {
if scale <= 0 {
scale = 1
}
size := len(a.Index)
if size == 0 {
size = len(a.Frame)
}
g := &gif.GIF{
Image: make([]*image.Paletted, size),
Delay: make([]int, size),
}
var palette color.Palette
if coloring != nil && coloring.Palette != nil {
palette = coloring.Palette.Colors
} 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)
}
}
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 {
for j, i := range a.Index {
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)
}
}
g.Delay[j] = int(f.Delay / time.Millisecond / 10)
g.Image[i] = toPaletted(Panel{
PalettedImage: &FrameImage{
Frame: f,
Palette: palette,
},
Dot: int(scale),
}, palette)
}
}
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[:]))
}
}

+ 342
- 0
animation_pin2dmd.go View File

@ -0,0 +1,342 @@
package dmd
import (
"bufio"
"bytes"
"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[uint8]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[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 = readBEString(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] = &Frame{
Width: a.Width,
Height: 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 {
f.Hash = make([]byte, 4)
if _, err = io.ReadFull(r, f.Hash); err != nil {
return
}
}
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
}

+ 114
- 0
animation_rundmd.go View File

@ -0,0 +1,114 @@
package dmd
import (
"io"
"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)
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
}
// Read interleaved (4-bits) palette indexes.
if _, err = io.ReadFull(rs, b); err != nil {
return
}
// Deinterleave.
p := &Plane{
Data: make([]byte, a.Width*a.Height),
}
for j, v := range b {
q := j << 1
p.Data[q+0] = v >> 4
p.Data[q+1] = v & 0x0f
}
a.Frame = append(a.Frame, &Frame{
Delay: delays[i],
Bits: 4,
Plane: []*Plane{p},
})
}
return
}

+ 206
- 0
animation_test.go View File

@ -0,0 +1,206 @@
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, 1)); 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 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++ {
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()
}
}
}

+ 238
- 0
animation_vni.go View File

@ -0,0 +1,238 @@
package dmd
import (
"bufio"
"encoding/binary"
"image/color"
"io"
"io/ioutil"
"log"
"time"
)
type vniAnimationHeader struct {
Cycles int16
Hold int16
ClockFrom int16
ClockSmall uint8
ClockInFront uint8
ClockOffsetX int16
ClockOffsetY int16
RefreshDelay uint16
Type uint8
Fsk uint8
Frames uint16
}
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
}
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
}
}
log.Printf("VPI file version %d, %d animations", version, nAnimations)
for i := 0; i < int(nAnimations); i++ {
a := new(Animation)
if err = a.readVPIData(r, int(version)); err != nil {
return
}
all = append(all, a)
}
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 {
return
}
if nameLength > 0 {
nameBytes = make([]byte, nameLength)
if _, err = io.ReadFull(r, nameBytes); err != nil {
return
}
a.Name = cString(nameBytes)
}
if err = binary.Read(r, binary.BigEndian, &header); err != nil {
return
}
log.Printf("reading VNI animation %q version %d: %#+v", a.Name, version, header)
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 version >= 3 {
// Edit mode.
if _, err = r.ReadByte(); err != nil {
return
}
}
if version >= 4 {
if a.Width, a.Height, err = readVPIDimensions(r); err != nil {
return
}
}
if err = readVPIFrames(r, a, int(header.Frames), version); err != nil {
return
}
// linear indexes
a.Index = make([]int, len(a.Frame))
for i := range a.Frame {
a.Index[i] = i
}
return
}
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,
}
if err = f.readVNIData(r, version); err != nil {
return
}
a.Frame = append(a.Frame, f)
}
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
}
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
}
if n <= 0 {
return int(i), 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,
}
}
return int(i), palette, nil
}
func (f *Frame) readVNIData(r *bufio.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
if version >= 4 {
f.Hash = make([]byte, 4)
if _, err = io.ReadFull(r, f.Hash); 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 {
return
}
if version < 3 {
return f.readVNIPlanes(r, int(planeSize))
}
return f.readVNIPlanesCompressed(r, int(planeSize))
}
func (f *Frame) readVNIPlanes(r *bufio.Reader, n int) (err error) {
for i := 0; i < int(f.Bits); i++ {
var marker byte
if marker, err = r.ReadByte(); err != nil {
return
}
if marker == 0x6d {
f.Mask = make([]byte, n)
if _, err = io.ReadFull(r, f.Mask); err != nil {
return
}
} else {
p := &Plane{
Marker: marker,
}
if err = p.readVNIData(r, n, 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)
}
return ErrHeatShrink
}
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
}

+ 65
- 0
display.go View File

@ -0,0 +1,65 @@
package dmd
import (
"image"
"image/color"
)
// Panel emulates a dot-matrix display panel.
type Panel struct {
image.PalettedImage
Dot int
Square bool
}
func MakePanel(p image.PalettedImage, size int) Panel {
return Panel{
PalettedImage: p,
Dot: size,
}
}
func (p Panel) At(x, y int) color.Color {
if p.Dot <= 1 {
return p.PalettedImage.At(x, y)
}
if !p.Square {
r1 := p.Dot / 2
xx := (x % p.Dot) - r1
x2 := xx * xx
yy := (y % p.Dot) - r1
y2 := yy * yy
r2 := r1 * r1
if x2+y2-r2 >= 0 {
return color.Transparent
}
}
x /= p.Dot
y /= p.Dot
return p.PalettedImage.At(x, y)
}
func (p Panel) ColorIndexAt(x, y int) uint8 {
if p.Dot <= 1 {
return p.PalettedImage.ColorIndexAt(x, y)
}
if !p.Square {
xx := x % p.Dot
yy := y % p.Dot
if xx*xx+yy*yy >= p.Dot*p.Dot {
return 0
}
}
x /= p.Dot
y /= p.Dot
return p.PalettedImage.ColorIndexAt(x, y)
}
func (p Panel) Bounds() image.Rectangle {
b := p.PalettedImage.Bounds()
b.Min.X *= p.Dot
b.Min.Y *= p.Dot
b.Max.X *= p.Dot
b.Max.Y *= p.Dot
return b
}

+ 7
- 0
doc.go View File

@ -0,0 +1,7 @@
/*
Package dmd implements well-known formats for dot-matrix display (DMD) art.
This dot-matrix display (DMD) image formats are most often used in arcade
machines, most notable pinball machines.
*/
package dmd

+ 97
- 0
frame.go View File

@ -0,0 +1,97 @@
package dmd
import (
"image"
"image/color"
"time"
)
type Frame struct {
Width, Height int
Delay time.Duration
Bits uint8 // Bit depth of the plane(s).
Plane []*Plane
PlaneData [][]byte
Mask []byte
Hash []byte
Checksum uint32
}
func (f *Frame) isOutOfBounds(x, y int) bool {
return x < 0 || y < 0 || x >= f.Width || y >= f.Height
}
func (f *Frame) isMaskRelevant() bool {
// (this.drawMask & 0x1) != 0x0 && this.frame.hasMask()
return len(f.Mask) > 0
}
const drawMask = 0x7fffff
func (f *Frame) ColorIndexAt(x, y int) uint8 {
if f.isOutOfBounds(x, y) {
return 0
}
var (
mask = uint8(128 >> (x % 8))
index uint8
bytesPerRow = f.Width >> 3
)
//log.Printf("x:%d y:%d mask:%#08b bytesPerRow:%d", x, y, mask, bytesPerRow)
if f.isMaskRelevant() {
if f.Mask[(x/8)+y*bytesPerRow]&mask != 0 {
index |= 1
}
}
var (
draw = drawMask >> 1
)
for i, plane := range f.Plane {
if ((1 << i) & draw) != 0 {
if plane.Data[(x/8)+y*bytesPerRow]&mask != 0 {
index |= 1 << i
}
}
}
return index
}
type FrameImage struct {
*Frame
color.Palette
}
func (i *FrameImage) At(x, y int) color.Color {
c := i.ColorIndexAt(x, y)
if i.Palette == nil {
return defaultPalettes[0][c]
}
return i.Palette[c]
}
func (i *FrameImage) Bounds() image.Rectangle {
switch len(i.Frame.Plane[0].Data) {
case 1568:
return image.Rect(0, 0, 196, 64)
case 512:
fallthrough
default:
return image.Rect(0, 0, 128, 32)
}
}
func (i *FrameImage) ColorIndexAt(x, y int) uint8 {
return i.Frame.ColorIndexAt(x, y)
}
func (i *FrameImage) ColorModel() color.Model {
return i.Palette
}
type Plane struct {
Marker byte
Data []byte
}
var _ image.PalettedImage = (*FrameImage)(nil)

+ 3
- 0
go.mod View File

@ -0,0 +1,3 @@
module maze.io/x/dmd
go 1.14

+ 363
- 0
internal/heatshrink/decoder.go View File

@ -0,0 +1,363 @@
package heatshrink
import (
"bytes"
"errors"
)
const (
minWindowBits = 4
maxWindowBits = 15
minLookaheadBits = 3
)
/* States for the polling state machine. */
const (
stateTagBit = iota /* tag bit */
stateYieldLiteral /* ready to yield literal byte */
stateBackrefIndexMSB /* most significant byte of index */
stateBackrefIndexLSB /* least significant byte of index */
stateBackrefCountMSB /* most significant byte of count */
stateBackrefCountLSB /* least significant byte of count */
stateYieldBackref /* ready to yield back-reference */
)
const (
sinkOK = 0 /* data sunk, ready to poll */
sinkFull = 1 /* out of space in internal buffer */
pollEmpty = 0 /* input exhausted */
pollMore = 1 /* more data remaining, call again w/ fresh output buffer */
pollErrorUnknown = -2
finishDone = 0 /* output is done */
finishMore = 1 /* more output remains */
)
const (
noBits = uint16(0xffff)
)
type decoder struct {
inputSize uint16 /* bytes in input buffer */
inputIndex uint16 /* offset to next unprocessed input byte */
outputCount uint16 /* how many bytes to output */
outputIndex uint16 /* index for bytes to output */
headIndex uint16 /* head of window buffer */
state uint8 /* current state machine node */
currentByte uint8 /* current byte of input */
bitIndex uint8 /* current bit index */
/* Fields that are only used if dynamically allocated. */
windowSz2 uint8 /* window buffer bits */
lookaheadSz2 uint8 /* lookahead bits */
/* Input buffer, then expansion window buffer */
decbuf []byte
inbuf []byte
outbuf bytes.Buffer
}
func Decompress(window, lookahead uint8, data []byte) ([]byte, error) {
var (
hsd = newDecoder(window, lookahead)
size = len(data)
read int
)
for {
_, tmp := hsd.sink(data[read:])
read += int(tmp)
if _, err := hsd.poll(); err != nil {
return nil, err
}
if read == size {
if hsd.finish() == finishDone {
break
}
}
}
return hsd.outbuf.Bytes(), nil
}
func newDecoder(windowSz2, lookaheadSz2 uint8) *decoder {
if (windowSz2 < minWindowBits) ||
(windowSz2 > maxWindowBits) ||
(lookaheadSz2 < minLookaheadBits) ||
(lookaheadSz2 >= windowSz2) {
return nil
}
hsd := &decoder{
windowSz2: windowSz2,
lookaheadSz2: lookaheadSz2,
decbuf: make([]byte, 1<<windowSz2),
inbuf: make([]byte, 65535),
}
hsd.reset()
return hsd
}
func (hsd *decoder) reset() {
hsd.state = stateTagBit
hsd.inputSize = 0
hsd.inputIndex = 0
hsd.bitIndex = 0x00
hsd.currentByte = 0x00
hsd.outputCount = 0
hsd.outputIndex = 0
hsd.headIndex = 0
hsd.outbuf.Reset()
}
/* Copy SIZE bytes into the decoder's input buffer, if it will fit. */
func (hsd *decoder) sink(data []byte) (result int, inputSize uint16) {
rem := uint16(len(hsd.inbuf)) - hsd.inputSize
if rem == 0 {
return sinkFull, 0
}
size := rem
if len(data) < int(size) {
size = uint16(len(data))
}
//log.Printf("-- sinking %v bytes\n", size)
/* copy into input buffer (at head of buffers) */
copy(hsd.inbuf[hsd.inputSize:], data[:size])
hsd.inputSize += size
return sinkOK, size
}
func (hsd *decoder) poll() (int, error) {
for {
//log.Printf("-- poll, state is %v, inputSize %v\n", hsd.state, hsd.inputSize)
var (
inState = hsd.state
err error
)
switch inState {
case stateTagBit:
hsd.state = hsd.dstTagBit()
case stateYieldLiteral:
hsd.state = hsd.dstYieldLiteral()
case stateBackrefIndexMSB:
hsd.state, err = hsd.dstBackrefIndexMSB()
case stateBackrefIndexLSB:
hsd.state = hsd.dstBackrefIndexLSB()
case stateBackrefCountMSB:
hsd.state, err = hsd.dstBackrefCountMSB()
case stateBackrefCountLSB:
hsd.state = hsd.dstBackrefCountLSB()
case stateYieldBackref:
hsd.state = hsd.dstYieldBackref()
default:
return pollErrorUnknown, nil
}
if err != nil {
return 0, err
}
/* If the current state cannot advance, check if input or output
* buffer are exhausted. */
if hsd.state == inState {
return pollEmpty, nil
}
}
}
func (hsd *decoder) finish() int {
switch hsd.state {
case stateTagBit,
/* If we want to finish with no input, but are in these states, it's
* because the 0-bit padding to the last byte looks like a backref
* marker bit followed by all 0s for index and count bits. */
stateBackrefIndexLSB,
stateBackrefIndexMSB,
stateBackrefCountLSB,
stateBackrefCountMSB,
/* If the output stream is padded with 0xFFs (possibly due to being in
* flash memory), also explicitly check the input size rather than
* uselessly returning MORE but yielding 0 bytes when polling. */
stateYieldLiteral:
if hsd.inputSize == 0 {
return finishDone
} else {
return finishMore
}
}
return finishMore
}
func (hsd *decoder) dstTagBit() uint8 {
bits := hsd.getBits(1) // get tag bit
if bits == noBits {
return stateTagBit
} else if bits > 0 {
return stateYieldLiteral
} else if hsd.windowSz2 > 8 {
return stateBackrefIndexMSB
} else {
hsd.outputIndex = 0
return stateBackrefIndexLSB
}
}
func (hsd *decoder) dstYieldLiteral() uint8 {
/* Emit a repeated section from the window buffer, and add it (again)
* to the window buffer. (Note that the repetition can include
* itself.)*/
bits := hsd.getBits(8)
if bits == noBits {
return stateYieldLiteral
} /* out of input */
mask := uint16(1<<hsd.windowSz2) - 1
c := uint8(bits & 0xFF)
// log.Printf("-- emitting literal byte 0x%02x\n", c)
hsd.decbuf[hsd.headIndex&mask] = c
hsd.headIndex++
hsd.pushByte(c)
return stateTagBit
}
func (hsd *decoder) dstBackrefIndexMSB() (uint8, error) {
bitCt := hsd.windowSz2
if bitCt <= 8 {
return 0, errors.New("heatshrink: bit count failed")
}
bits := hsd.getBits(bitCt - 8)
// log.Printf("-- backref index (msb), got 0x%04x (+1)\n", bits)
if bits == noBits {
return stateBackrefIndexMSB, nil
}
hsd.outputIndex = bits << 8
return stateBackrefIndexLSB, nil
}
func (hsd *decoder) dstBackrefIndexLSB() uint8 {
bitCt := hsd.windowSz2
if bitCt > 8 {
bitCt = 8
}
bits := hsd.getBits(bitCt)
// log.Printf("-- backref index (lsb), got 0x%04x (+1)\n", bits)
if bits == noBits {
return stateBackrefIndexLSB
}
hsd.outputIndex |= bits
hsd.outputIndex++
brBitCt := hsd.lookaheadSz2
hsd.outputCount = 0
if brBitCt > 8 {
return stateBackrefCountMSB
} else {
return stateBackrefCountLSB
}
}
func (hsd *decoder) dstBackrefCountMSB() (uint8, error) {
brBitCt := hsd.lookaheadSz2
if brBitCt <= 8 {
return 0, errors.New("heatshrink: bit count failed")
}
bits := hsd.getBits(brBitCt - 8)
// log.Printf("-- backref count (msb), got 0x%04x (+1)\n", bits)
if bits == noBits {
return stateBackrefCountMSB, nil
}
hsd.outputCount = bits << 8
return stateBackrefCountLSB, nil
}
func (hsd *decoder) dstBackrefCountLSB() uint8 {
brBitCt := hsd.lookaheadSz2
if brBitCt > 8 {
brBitCt = 8
}
bits := hsd.getBits(brBitCt)
// log.Printf("-- backref count (lsb), got 0x%04x (+1)\n", bits)
if bits == noBits {
return stateBackrefCountLSB
}
hsd.outputCount |= bits
hsd.outputCount++
return stateYieldBackref
}
func (hsd *decoder) dstYieldBackref() uint8 {
count := hsd.outputCount
mask := uint16(1<<hsd.windowSz2) - 1
negOffset := hsd.outputIndex
// log.Printf("-- emitting %v bytes from -%v bytes back\n", count, negOffset)
if negOffset > mask+1 {
// log.Fatal("neg_offset assert failed.")
return 0
}
if count > (1 << hsd.lookaheadSz2) {
// log.Fatal("count assert failed.")
return 0
}
for i := uint16(0); i < count; i++ {
c := hsd.decbuf[(hsd.headIndex-negOffset)&mask]
hsd.pushByte(c)
hsd.decbuf[hsd.headIndex&mask] = c
hsd.headIndex++
// log.Printf(" -- ++ 0x%02x\n", c)
}
hsd.outputCount -= count
if hsd.outputCount == 0 {
return stateTagBit
}
return stateYieldBackref
}
/* Get the next COUNT bits from the input buffer, saving incremental progress.
* Returns noBits on end of input, or if more than 15 bits are requested. */
func (hsd *decoder) getBits(count uint8) uint16 {
accumulator := uint16(0)
if count > 15 {
return noBits
}
// log.Printf("-- popping %v bit(s)\n", count)
/* If we aren't able to get COUNT bits, suspend immediately, because we
* don't track how many bits of COUNT we've accumulated before suspend. */
if hsd.inputSize == 0 {
if hsd.bitIndex < (1 << (count - 1)) {
return noBits
}
}
for i := uint8(0); i < count; i++ {
if hsd.bitIndex == 0x00 {
if hsd.inputSize == 0 {
// log.Printf(" -- out of bits, suspending w/ accumulator of %v (0x%02x)\n", accumulator, accumulator)
return noBits
}
hsd.currentByte = hsd.inbuf[hsd.inputIndex]
hsd.inputIndex++
// log.Printf(" -- pulled byte 0x%02x\n", hsd.currentByte)
if hsd.inputIndex == hsd.inputSize {
hsd.inputIndex = 0 /* input is exhausted */
hsd.inputSize = 0
}
hsd.bitIndex = 0x80
}
accumulator <<= 1
if hsd.currentByte&hsd.bitIndex > 0 {
accumulator |= 0x01
}
hsd.bitIndex >>= 1
}
/*
if count > 1 {
log.Printf(" -- accumulated %08x\n", accumulator)
}
*/
return accumulator
}
func (hsd *decoder) pushByte(c uint8) {
// log.Printf(" -- pushing c: 0x%02x\n", c)
hsd.outbuf.WriteByte(c)
}

+ 252
- 0
palette.go View File

@ -0,0 +1,252 @@
package dmd
import (
"bufio"
"encoding/binary"
"fmt"
"image"
"image/color"
"image/draw"
"io"
"io/ioutil"
"os"
)
type Coloring struct {
Version uint8
Palette *Palette
Palettes []*Palette
Default int
Mappings map[uint32]*Mapping
Masks [][]byte
}
func (c *Coloring) GetPalette(i int) *Palette {
if i < len(c.Palettes) {
return c.Palettes[i]
}
if c.Default < len(c.Palettes) {
return c.Palettes[c.Default]
}
return c.Palette
}
func (c *Coloring) readPalettes(r *bufio.Reader) (err error) {
var nPalettes uint16
if err = binary.Read(r, binary.BigEndian, &nPalettes); err != nil {
return
}
c.Palettes = make([]*Palette, nPalettes)
for i := 0; i < int(nPalettes); i++ {
if c.Palettes[i], err = ReadPalette(r); err != nil {
return
}
// log.Printf("palette %d: %d colors, default:%t persistent:%t", i, len(c.Palettes[i].Colors), c.Palettes[i].IsDefault, c.Palettes[i].IsPersistent)
if c.Palette == nil && c.Palettes[i].IsDefault {
c.Palette = c.Palettes[i]
c.Default = i
}
}
if c.Palette == nil && len(c.Palettes) > 0 {
c.Palette = c.Palettes[0]
}
return
}
func (c *Coloring) readMappings(r *bufio.Reader) (err error) {
var nMappings uint16
if err = binary.Read(r, binary.BigEndian, &nMappings); err != nil {
return
}
c.Mappings = make(map[uint32]*Mapping)
for i := 0; i < int(nMappings); i++ {
var m *Mapping
if m, err = ReadMapping(r); err != nil {
return
}
c.Mappings[m.Checksum] = m
}
return
}
func (c *Coloring) readMasks(r *bufio.Reader) (