Package dmd implements well-known formats for dot-matrix display (DMD) art.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

588 lines
15 KiB

package dmd
import (
"bytes"
"encoding/binary"
"fmt"
"image"
"io"
"io/ioutil"
"log"
"time"
"maze.io/x/dmd/bitmap/color"
"maze.io/x/dmd/internal/heatshrink"
)
// DecodeVPIN calls DecodeVPINWithColoring.
func DecodeVPIN(r io.ReadSeeker) (Animations, error) {
return DecodeVPINWithColoring(r, nil)
}
// DecodeVPINWithColoring can decode animations from a Virtual Pinball animation file.
//
// Typically, these animation file are called "pin2dmd.vni" or "pin2dmd.ani". If you
// have a machine palette file ("pin2dmd.pal"), you can load the coloring definitions
// with LoadPin2DMDColoring / DecodePin2DMDColoring.
func DecodeVPINWithColoring(r io.ReadSeeker, c *Pin2DMDColoring) (Animations, error) {
var (
magic [4]byte
err error
)
if _, err = io.ReadFull(r, magic[:]); err != nil {
return nil, err
}
switch string(magic[:]) {
case "ANIM":
case "VPIN":
default:
return nil, ErrHeaderMagic
}
var version int16
if err = binary.Read(r, binary.BigEndian, &version); err != nil {
return nil, err
}
var animations int16
if err = binary.Read(r, binary.BigEndian, &animations); err != nil {
return nil, err
}
if version >= 2 {
// Skip over animation indexes.
discard := 4 * int64(animations)
if _, err = io.Copy(ioutil.Discard, io.LimitReader(r, discard)); err != nil {
return nil, err
}
}
all := make([]Animation, animations)
for i := 0; i < int(animations); i++ {
//if os.Getenv("DECODE") != "" {
if all[i], err = decodeVPNAnimation(i, r, version, c); err != nil {
return nil, err
}
//} else {
// all[i], err = parseVPINAnimation(i, r, version, c)
//}
/*
if all[i], err = parseVPINAnimation(r, version, c); err != nil {
if all[i], err = decodeVPNAnimation(r, version, c); err != nil {
return nil, err
}
*/
if err != nil {
return nil, err
}
}
return all, nil
}
type vpinAnimationHeader struct {
Cycles int16
Hold int16
ClockFrom int16
ClockSmall bool
ClockInFront bool
ClockOffsetX int16
ClockOffsetY int16
RefreshDelay int16
Type uint8
FSK uint8
Frames int16
}
type vpinAnimation struct {
n int
r io.ReadSeeker
width, height int
name string
header vpinAnimationHeader
frame *image.Paletted
frames int
pos int
info []*vpinFrameInfo
palette color.Palette
}
type vpinFrameInfo struct {
header vpinFrameHeader
offset int64
compressedSize int32
bits uint8
}
func (a vpinAnimation) Name() string { return a.name }
func (a vpinAnimation) Len() int { return a.frames }
func (a vpinAnimation) Size() (width, height int) { return a.width, a.height }
func (a vpinAnimation) IsMask() bool { return a.palette == nil || isMask(a.palette) }
func (a vpinAnimation) ClockInFront() bool { return a.header.ClockInFront }
func (a vpinAnimation) ClockOffset() image.Point {
return image.Pt(int(a.header.ClockOffsetX), int(a.header.ClockOffsetY))
}
func (a *vpinAnimation) Duration() (total time.Duration) {
log.Printf("vpin %s: %+v", a.name, a.info)
for _, info := range a.info {
total += time.Duration(info.header.Delay)
}
log.Printf("vpin: duration %d -> %s", int64(total), total*time.Millisecond)
return total * time.Millisecond
}
func (a *vpinAnimation) NextFrame() (frame image.Image, delay time.Duration, err error) {
if a.pos+1 > a.frames {
log.Printf("dmd: VPIN animation %d next %d is beyond %d: EOF on %#+v", a.n, a.pos+1, a.frames, a)
return nil, 0, io.EOF
}
info := a.info[a.pos]
log.Printf("vpin: next frame, pos:%d frames:%d info@%p:%+v", a.pos, a.frames, info, info)
a.pos++
log.Printf("vpin: animation %d seek to %d", a.n, info.offset)
if _, err = a.r.Seek(info.offset, io.SeekStart); err != nil {
return
}
//var header vpinFrameHeader
//if err = binary.Read(a.r, binary.BigEndian, &header); err != nil {
// return
//}
if info.compressedSize > 0 {
var (
comp = make([]byte, info.compressedSize)
data []byte
)
if _, err = io.ReadFull(a.r, comp); err != nil {
return
}
if data, err = heatshrink.Decompress(10, 0, comp); err != nil {
return
}
if err = decodeVPINFramePlanes(bytes.NewReader(data), a.frame, info.bits, info.header.PlaneSize, nil); err != nil {
return
}
} else if err = decodeVPINFramePlanes(a.r, a.frame, info.bits, info.header.PlaneSize, nil); err != nil {
return
}
a.frame.Palette = a.palette
return a.frame, time.Duration(info.header.Delay) * time.Millisecond, nil
}
func (a *vpinAnimation) SeekFrame(pos int) error {
if pos < 0 || pos >= a.frames {
return ErrSeek
}
a.pos = pos
return nil
}
type vpinFrameHeader struct {
PlaneSize int16
Delay uint16
}
func parseVPINAnimation(n int, r io.ReadSeeker, version int16, c *Pin2DMDColoring) (Animation, error) {
var (
a = &vpinAnimation{
n: n,
r: r,
}
nameLen int16
name []byte
offset int64
err error
)
if err = binary.Read(r, binary.BigEndian, &nameLen); err != nil {
return nil, err
}
if nameLen > 0 {
if name, err = ioutil.ReadAll(io.LimitReader(r, int64(nameLen))); err != nil {
return nil, err
}
a.name = string(name)
}
if err = binary.Read(r, binary.BigEndian, &a.header); err != nil {
return nil, err
}
offset, _ = r.Seek(0, io.SeekCurrent)
log.Printf("position after header of animation %d: %d", n, offset)
a.frames = int(a.header.Frames)
if a.frames < 0 {
a.frames += 65336
}
a.info = make([]*vpinFrameInfo, a.frames)
if version >= 2 {
var index int
if index, err = decodeVPINPalettesAndColors(r, &a.palette); err != nil {
return nil, err
}
if index > 0 && c != nil {
// log.Printf("using palette index %d", index)
a.palette = c.Palettes[index].Colors
}
}
if a.palette == nil {
a.palette = Mask
}
if version >= 3 {
var b [1]byte
if _, err = io.ReadFull(r, b[:]); err != nil {
return nil, err
}
}
if version >= 4 {
var size struct {
Width, Height int16
}
if err = binary.Read(r, binary.BigEndian, &size); err != nil {
return nil, err
}
a.width = int(size.Width)
a.height = int(size.Height)
} else {
a.width = 128
a.height = 32
}
a.frame = image.NewPaletted(image.Rectangle{Max: image.Point{X: a.width, Y: a.height}}, nil)
if version >= 5 {
var skip int16
if err = binary.Read(r, binary.BigEndian, &skip); err != nil {
return nil, err
}
if _, err = io.Copy(ioutil.Discard, io.LimitReader(r, int64(skip)*3)); err != nil {
return nil, err
}
}
if offset, err = r.Seek(0, io.SeekCurrent); err != nil {
return nil, fmt.Errorf("dmd: error getting current VPiN position: %w", err)
}
log.Println("position before frames:", offset)
for i := 0; i < a.frames; i++ {
var (
header vpinFrameHeader
bits uint8
frameStart int64
compressedSize int32
)
if _, err = r.Seek(offset, io.SeekStart); err != nil {
return nil, fmt.Errorf("dmd: error skipping to VPIN animation %d: %w", i+1, err)
}
if err = binary.Read(r, binary.BigEndian, &header); err != nil {
return nil, fmt.Errorf("dmd: error reading VPIN animation %d header: %w", i+1, err)
}
offset += int64(binary.Size(header))
if version >= 4 {
if _, err = io.Copy(ioutil.Discard, io.LimitReader(r, 4)); err != nil {
return nil, err
}
offset += 4 // checksum
}
if err = binary.Read(r, binary.BigEndian, &bits); err != nil {
return nil, err
}
offset++
var compressed bool
if version >= 3 {
if err = binary.Read(r, binary.BigEndian, &compressed); err != nil {
return nil, err
}
offset++ // compressed flag
}
frameStart = offset
log.Printf("frame header: %+v, bits %d, offset %d", header, bits, frameStart)
if compressed {
if err = binary.Read(r, binary.BigEndian, &compressedSize); err != nil {
return nil, err
}
offset += 4
log.Println("position before compressed frame", i+1, ":", offset)
offset += int64(compressedSize)
} else {
log.Println("position before frame", i+1, ":", offset)
offset += int64(bits) * int64(header.PlaneSize+1)
}
log.Printf("delay of frame %d: %s", i+1, time.Duration(header.Delay)*time.Millisecond)
log.Println("position after frame", i+1, ":", offset)
a.info[i] = &vpinFrameInfo{
header: header,
offset: frameStart,
compressedSize: compressedSize,
bits: bits,
}
log.Printf("info at %d/%d (%p): %+v", i, a.frames, a.info[i], a.info[i])
}
if _, err = r.Seek(offset, io.SeekStart); err != nil {
return nil, err
}
return a, nil
}
func decodeVPNAnimation(n int, r io.ReadSeeker, version int16, c *Pin2DMDColoring) (Animation, error) {
var (
a = &imageAnimation{
width: 128,
height: 32,
}
nameLength int16
name []byte
err error
)
if err = binary.Read(r, binary.BigEndian, &nameLength); err != nil {
return nil, err
}
if nameLength > 0 {
name = make([]byte, nameLength)
if _, err = io.ReadFull(r, name); err != nil {
return nil, err
}
a.name = string(name)
}
var header vpinAnimationHeader
if err = binary.Read(r, binary.BigEndian, &header); err != nil {
return nil, err
}
a.clockInFront = header.ClockInFront
a.clockOffset = image.Point{
X: int(header.ClockOffsetX),
Y: int(header.ClockOffsetY),
}
//offset, _ := r.Seek(0, io.SeekCurrent)
//log.Printf("position after header of animation %d: %d", n, offset)
/*
if a.clockInFront || a.clockOffset.X+a.clockOffset.Y != 0 {
log.Printf("vpin: version %d animation %q header: %+v", version, a.name, header)
}
*/
var frames = int(header.Frames)
if frames < 0 {
frames += 65536
}
if version >= 2 {
var index int
if index, err = decodeVPINPalettesAndColors(r, &a.palette); err != nil {
return nil, err
}
if index > 0 && c != nil {
// log.Printf("using palette index %d", index)
a.palette = c.Palettes[index].Colors
}
}
if version >= 3 {
var b [1]byte
if _, err = io.ReadFull(r, b[:]); err != nil {
return nil, err
}
}
if version >= 4 {
var size struct {
Width, Height int16
}
if err = binary.Read(r, binary.BigEndian, &size); err != nil {
return nil, err
}
a.width = int(size.Width)
a.height = int(size.Height)
}
if version >= 5 {
if err = decodeVPINMasks(r, a); err != nil {
return nil, err
}
}
//offset, _ = r.Seek(0, io.SeekCurrent)
//log.Println("position before frames:", offset)
a.delay = make([]time.Duration, frames)
a.frame = make([]image.Image, frames)
for i := 0; i < frames; i++ {
//offset, _ := r.(io.Seeker).Seek(0, io.SeekCurrent)
//log.Printf("position before frame %d: %d", i+1, offset)
if a.frame[i], a.delay[i], err = decodeVPINFrame(r, a.width, a.height, version, c); err != nil {
return nil, err
}
//log.Printf("delay of frame %d: %s", i+1, a.delay[i])
if a.frame[i].(*image.Paletted).Palette == nil {
a.frame[i].(*image.Paletted).Palette = a.palette
} else {
// Key frame switched to custom palette.
a.palette = a.frame[i].(*image.Paletted).Palette
}
//offset, _ = r.(io.Seeker).Seek(0, io.SeekCurrent)
//log.Printf("position before frame %d: %d", i+1, offset)
}
return a, nil
}
func decodeVPINPalettesAndColors(r io.Reader, p *color.Palette) (index int, err error) {
var header struct {
Index int16
Colors int16
}
if err = binary.Read(r, binary.BigEndian, &header); err != nil {
return
}
index = int(header.Index)
// log.Printf("vpin: version palette header: %+v", header)
if header.Colors <= 0 {
return
}
*p = make(color.Palette, header.Colors)
for i := 0; i < int(header.Colors); i++ {
if (*p)[i], err = color.DecodeRGB24(r); err != nil {
return
}
}
return
}
func decodeVPINMasks(r io.Reader, a *imageAnimation) (err error) {
var masks int16
if err = binary.Read(r, binary.BigEndian, &masks); err != nil {
return
}
for i := 0; i < int(masks); i++ {
var header struct {
B byte
Size int16
}
if err = binary.Read(r, binary.BigEndian, &header); err != nil {
return
}
if _, err = ioutil.ReadAll(io.LimitReader(r, int64(header.Size))); err != nil {
return
}
}
return
}
func decodeVPINFrame(r io.Reader, width, height int, version int16, c *Pin2DMDColoring) (frame *image.Paletted, delay time.Duration, err error) {
var (
header vpinFrameHeader
bits uint8
)
if err = binary.Read(r, binary.BigEndian, &header); err != nil {
return
}
delay = time.Duration(header.Delay) * time.Millisecond
// log.Printf("vpin: version %d frame header: %+v", version, header)
if version >= 4 {
var checksum uint32
if err = binary.Read(r, binary.BigEndian, &checksum); err != nil {
return
}
}
if err = binary.Read(r, binary.BigEndian, &bits); err != nil {
return
}
//offset, _ := r.(io.Seeker).Seek(0, io.SeekCurrent)
//log.Printf("frame header: %+v, bits %d, offset %d", header, bits, offset)
//frame := NewRGBA16Image(image.Rect(0, 0, width, height))
frame = image.NewPaletted(image.Rect(0, 0, width, height), nil)
if version < 3 {
err = decodeVPINFramePlanes(r, frame, bits, header.PlaneSize, c)
return
}
var isCompressed bool
if err = binary.Read(r, binary.BigEndian, &isCompressed); err != nil {
return
}
if isCompressed {
var (
size int32
comp, data []byte
)
if err = binary.Read(r, binary.BigEndian, &size); err != nil {
return
}
//offset, _ := r.(io.Seeker).Seek(0, io.SeekCurrent)
//log.Printf("position before frame: %d", offset)
comp = make([]byte, size)
if _, err = io.ReadFull(r, comp); err != nil {
return
}
if data, err = heatshrink.Decompress(10, 0, comp); err != nil {
return
}
err = decodeVPINFramePlanes(bytes.NewReader(data), frame, bits, header.PlaneSize, c)
return
}
//offset, _ = r.(io.Seeker).Seek(0, io.SeekCurrent)
//log.Printf("position before frame: %d", offset)
err = decodeVPINFramePlanes(r, frame, bits, header.PlaneSize, c)
return
}
func decodeVPINFramePlanes(r io.Reader, frame *image.Paletted, bits uint8, size int16, c *Pin2DMDColoring) (err error) {
var (
plane = make([]byte, size)
mask = make([]byte, size)
)
for i := range mask {
mask[i] = 0xff
}
for i := 0; i < int(bits); i++ {
var marker byte
if err = binary.Read(r, binary.BigEndian, &marker); err != nil {
return
}
// log.Printf("dmd: VPIN plane bit %d marker %#02x", i, marker)
switch marker {
case 0x00, 0x01, 0x02, 0x03:
if _, err = io.ReadFull(r, plane); err != nil {
return
}
// offset, _ := r.(io.Seeker).Seek(0, io.SeekCurrent)
// fmt.Printf("plane %d@%d:\n%s", marker, offset, hex.Dump(plane))
if c != nil {
// Colorize keyframe based on checksum.
if checksum := calcChecksumWithMask(plane, mask, true); c.Mappings[checksum] != nil && c.Mappings[checksum].PaletteIndex <= uint16(len(c.Palettes)) {
frame.Palette = c.Palettes[c.Mappings[checksum].PaletteIndex].Colors
} else if checksum = calcChecksum(plane, true); c.Mappings[checksum] != nil && c.Mappings[checksum].PaletteIndex <= uint16(len(c.Palettes)) {
frame.Palette = c.Palettes[c.Mappings[checksum].PaletteIndex].Colors
}
}
for i, v := range plane {
for b := 0; b < 8; b++ {
if mask[i]&(1<<(7-b)) == 0 {
continue
}
if v&(1<<(7-b)) != 0 {
frame.Pix[i*8+b] |= 1 << marker
}
}
}
default:
return fmt.Errorf("dmd: unexpected VPIN frame marker %#02x", marker)
case 0x6d:
if _, err = io.ReadFull(r, mask); err != nil {
return
}
// fmt.Print(hex.Dump(mask))
}
}
return
}