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.

244 lines
6.7 KiB

package dmd
import (
"encoding/binary"
"fmt"
"image"
"io"
"time"
)
const (
runDMDPanelWidth = 128
runDMDPanelHeight = 32
runDMDHeaderMagic = "DGD"
runDMDHeaderOffset = 0xc800
runDMDHeaderBuildOffset = 0x01ef
runDMDHeaderSize = 0x0200
runDMDBitmapSize = 128 * 32 / 2
)
// DecodeRunDMD can decode animations in a (raw) Run-DMD SD card image.
func DecodeRunDMD(r io.ReadSeeker) (Animations, error) {
var magic [3]byte
if _, err := io.ReadFull(r, magic[:]); err != nil {
return nil, err
} else if string(magic[:]) != runDMDHeaderMagic {
return nil, ErrHeaderMagic
}
var size uint16
if err := binary.Read(r, binary.BigEndian, &size); err != nil {
return nil, err
}
var next uint16
if err := binary.Read(r, binary.BigEndian, &next); err != nil {
return nil, err
}
var build [8]byte
if _, err := r.Seek(runDMDHeaderBuildOffset, io.SeekStart); err != nil {
return nil, err
}
if _, err := io.ReadFull(r, build[:]); err != nil {
return nil, err
}
// log.Printf("dmd: expecting %d RunDMD animations, build %s, number %d (%#04x)", size, cString(build[:]), next, next)
var (
all = make(Animations, size)
err error
)
for i := range all {
if all[i], err = decodeRunDMDAnimation(r, i); err != nil {
return nil, err
}
}
return all, nil
}
type runDMDAnimation struct {
r io.ReadSeeker
name string
width, height int
pos int
delay []time.Duration
index []int
frames int
offset int64
pixels [runDMDBitmapSize]byte
frame *image.Paletted
}
func (r *runDMDAnimation) Name() string { return r.name }
func (r *runDMDAnimation) IsMask() bool { return true }
func (r *runDMDAnimation) Size() (width, height int) { return 128, 32 }
func (r *runDMDAnimation) ClockInFront() bool { return false }
func (r *runDMDAnimation) ClockIsSmall() bool { return false }
func (r *runDMDAnimation) ClockOffset() image.Point { return image.Point{} }
func (r *runDMDAnimation) Duration() (total time.Duration) {
for _, i := range r.index {
if i >= 0 {
total += r.delay[i]
}
}
return
}
func (r *runDMDAnimation) Len() int {
var valid int
for _, index := range r.index {
if index >= 0 {
valid++
}
}
return valid
}
func (r *runDMDAnimation) NextFrame() (image.Image, time.Duration, error) {
index := -1
for index < 0 {
if r.pos+1 > len(r.index) {
return nil, 0, io.EOF
}
index = r.index[r.pos]
r.pos++
}
// Skip to frame
stride := r.width * r.height / 2
//if _, err := r.r.Seek(runDMDHeaderOffset+r.offset+int64(index)*runDMDBitmapSize, io.SeekStart); err != nil {
if _, err := r.r.Seek(runDMDHeaderSize+r.offset*runDMDHeaderSize+int64(index)*int64(stride), io.SeekStart); err != nil {
return nil, 0, fmt.Errorf("dmd: error seeking to animation %d frame data: %w", r.pos-1, err)
}
// Read frame
if _, err := io.ReadFull(r.r, r.pixels[:]); err != nil {
return nil, 0, fmt.Errorf("dmd: error reading animation %d frame pixels: %w", r.pos-1, err)
}
for j, v := range r.pixels[:] {
o := j << 1
r.frame.Pix[o+0] = v >> 4
r.frame.Pix[o+1] = v & 0xf
}
return r.frame, r.delay[index], nil
}
func (r *runDMDAnimation) SeekFrame(pos int) error {
if pos < 0 || pos >= len(r.index) {
return ErrSeek
}
r.pos = pos
return nil
}
type runDMDHeader struct {
ID uint16
_ uint8
Frames uint8
ByteOffset uint32
Entries uint8
Width uint8
Height uint8
Unknown [9]byte
Name [32]byte
}
type runDMDFrameHeader struct {
ID uint8
Delay uint8
}
func decodeRunDMDAnimation(r io.ReadSeeker, i int) (Animation, error) {
if _, err := r.Seek(runDMDHeaderOffset+runDMDHeaderSize*int64(i), io.SeekStart); err != nil {
return nil, err
}
var header runDMDHeader
if err := binary.Read(r, binary.BigEndian, &header); err != nil {
return nil, fmt.Errorf("dmd: error seeking to animation %d: %w", i+1, err)
}
// log.Printf("animation %d: %q: %s", i, cString(header.Name[:]), hex.Dump(header.Unknown[:]))
a := &runDMDAnimation{
r: r,
name: cString(header.Name[:]),
width: int(header.Width),
height: int(header.Height),
frames: int(header.Frames),
offset: int64(header.ByteOffset), // * runDMDHeaderSize,
frame: image.NewPaletted(image.Rectangle{Max: image.Point{X: 128, Y: 32}}, Mask),
}
a.width = 128 // fixed
// Read indexes and delays.
if _, err := r.Seek(int64(header.ByteOffset)*runDMDHeaderSize, io.SeekStart); err != nil {
return nil, fmt.Errorf("dmd: error seeking to animation %d delays: %w", i+1, err)
}
for i := 0; i < int(header.Frames); i++ {
var frameHeader runDMDFrameHeader
if err := binary.Read(r, binary.BigEndian, &frameHeader); err != nil {
return nil, err
}
a.delay = append(a.delay, time.Duration(frameHeader.Delay)*time.Millisecond)
a.index = append(a.index, int(frameHeader.ID)-1)
}
return a, nil
}
func decodeRunDMDAnimationOld(r io.ReadSeeker, i int) (Animation, error) {
var header runDMDHeader
if err := binary.Read(r, binary.BigEndian, &header); err != nil {
return nil, fmt.Errorf("dmd: error seeking to animation %d: %w", i+1, err)
}
// log.Printf("animation %d: %q: %s", i, cString(header.Name[:]), hex.Dump(header.Unknown[:]))
a := &imageAnimation{
name: cString(header.Name[:]),
isMask: true,
width: int(header.Width),
height: int(header.Height),
}
a.width = 128
// Read indexes and delays.
if _, err := r.Seek(int64(header.ByteOffset)*runDMDHeaderSize, io.SeekStart); err != nil {
return nil, fmt.Errorf("dmd: error seeking to animation %d delays: %w", i+1, err)
}
for i := 0; i < int(header.Frames); i++ {
var frameHeader runDMDFrameHeader
if err := binary.Read(r, binary.BigEndian, &frameHeader); err != nil {
return nil, err
}
a.delay = append(a.delay, time.Duration(frameHeader.Delay)*time.Millisecond)
a.index = append(a.index, int(frameHeader.ID)-1)
}
// Read frames.
if _, err := r.Seek(runDMDHeaderOffset+int64(header.ByteOffset), io.SeekStart); err != nil {
return nil, fmt.Errorf("dmd: error seeking to animation %d frames: %w", i+1, err)
}
var (
stride = a.width * a.height / 2
pixels = make([]byte, stride)
offset = runDMDHeaderSize + int64(header.ByteOffset)*runDMDHeaderSize
)
for i := 0; i < int(header.Frames); i++ {
if _, err := r.Seek(offset, io.SeekStart); err != nil {
return nil, fmt.Errorf("dmd: error seeking to animation %d frame data: %w", i+1, err)
}
offset += int64(stride)
f := image.NewPaletted(image.Rect(0, 0, a.width, a.height), Mask)
if _, err := r.Read(pixels); err != nil {
return nil, fmt.Errorf("dmd: error reading RunDMD frame %d/%d: %w", i+1, header.Frames, err)
}
for j, v := range pixels {
o := j << 1
f.Pix[o+0] = v >> 4
f.Pix[o+1] = v & 0xf
}
a.frame = append(a.frame, f)
}
return a, nil
}