ndi/ndi.go
2024-11-03 22:39:52 -08:00

219 lines
4.2 KiB
Go

package ndi
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"image"
"image/color"
"image/color/palette"
"io"
"log"
)
var (
ErrNotNDI = errors.New("ndi: not an ndi file")
ErrUnexpectedData = errors.New("ndi: unexpected data")
NDI1MagicBytes = []byte("ndi1")
Logger = log.New(log.Writer(), "ndi debug: ", 0)
)
const (
LUTColorStride = 3
HeaderStride = 12
LUTPadding = 4
)
func init() {
image.RegisterFormat("ndi", string(NDI1MagicBytes), Decode, DecodeConfig)
}
func Decode(r io.Reader) (o image.Image, err error) {
buf := bytes.Buffer{}
_, err = io.Copy(&buf, r)
if err != nil {
return o, fmt.Errorf("ndi: decode copy failed")
}
cfgBuf := bytes.NewBuffer(buf.Bytes())
cfg, err := DecodeConfig(cfgBuf)
if err != nil {
return o, fmt.Errorf("ndi: decode config failed: %w", err)
}
cfgBuf = bytes.NewBuffer(buf.Bytes())
palette, err := DecodePalette(cfgBuf)
if err != nil {
return o, fmt.Errorf("ndi: decode palette failed: %w", err)
}
bounds := image.Rect(0, 0, cfg.Width, cfg.Height)
img := image.NewPaletted(bounds, palette)
// skip until end of LUT
buf.Next(HeaderStride + (len(palette) * LUTColorStride) + LUTPadding)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
index, err := buf.ReadByte()
if err != nil {
// skip because we don't mind bad pixels
continue
}
img.SetColorIndex(x, y, uint8(index)%uint8(len(palette)))
}
}
return img, nil
}
func DecodeConfig(r io.Reader) (cfg image.Config, err error) {
imageBuf := bytes.Buffer{}
readBytes, err := io.Copy(&imageBuf, r)
if err != nil {
return cfg, fmt.Errorf("ndi: decode config copy failed")
}
header := imageBuf.Next(HeaderStride)
magicBytes := header[0:4]
widthBytes := header[4:6]
lutSizeByte := header[6]
if !bytes.Equal(magicBytes, NDI1MagicBytes) {
return cfg, ErrNotNDI
}
width := binary.BigEndian.Uint16(widthBytes)
lutByteWidth := int(lutSizeByte) * LUTColorStride
imagePixelCount := readBytes - ((int64(lutByteWidth) + LUTPadding) + HeaderStride)
height := imagePixelCount / int64(width)
cfg.ColorModel = color.RGBAModel
cfg.Width = int(width)
cfg.Height = int(height)
return cfg, nil
}
func DecodePalette(r io.Reader) (p color.Palette, err error) {
imageBuf := bytes.Buffer{}
_, err = io.Copy(&imageBuf, r)
if err != nil {
return
}
header := imageBuf.Next(HeaderStride)
lutCount := int(header[6])
tableSize := lutCount * LUTColorStride
tableBytes := imageBuf.Next(tableSize)
p = make(color.Palette, lutCount)
for index := 0; index < lutCount; index++ {
baseIndex := index * LUTColorStride
p[index] = color.RGBA{
R: tableBytes[baseIndex+0],
G: tableBytes[baseIndex+1],
B: tableBytes[baseIndex+2],
A: 0xFF,
}
}
return
}
func Encode(w io.Writer, img image.Image, cfg *Options) error {
if cfg == nil {
cfg = &Options{
Palette: palette.WebSafe,
}
}
_, err := w.Write(NDI1MagicBytes)
if err != nil {
return err
}
bounds := img.Bounds()
width := bounds.Max.X - bounds.Min.X
widthBytes := make([]byte, 2)
binary.BigEndian.PutUint16(widthBytes, uint16(width))
Logger.Println("width ", width)
Logger.Println("width bytes ", widthBytes)
_, err = w.Write(widthBytes)
if err != nil {
return err
}
err = encodeLUT(w, cfg)
if err != nil {
return err
}
w.Write([]byte("AWAW"))
err = encodePixels(w, img, cfg)
if err != nil {
return err
}
return nil
}
func encodeLUT(w io.Writer, cfg *Options) error {
if len(cfg.Palette) == 0 {
cfg.Palette = palette.WebSafe
}
_, err := w.Write([]byte{byte(len(cfg.Palette))})
if err != nil {
return err
}
w.Write([]byte("AWAWA"))
for _, color := range cfg.Palette {
r, g, b, _ := color.RGBA()
_, err = w.Write([]byte{
byte(r / 256),
byte(g / 256),
byte(b / 256),
})
if err != nil {
return err
}
}
return nil
}
func encodePixels(w io.Writer, img image.Image, cfg *Options) error {
bounds := img.Bounds()
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
color := img.At(x, y)
index := uint8(cfg.Palette.Index(color))
_, err := w.Write([]byte{index})
if err != nil {
return err
}
}
}
return nil
}
type Options struct {
Palette color.Palette
}