This commit is contained in:
41666 2024-11-03 22:39:52 -08:00
parent 7909fc9588
commit 988be32480
9 changed files with 421 additions and 1 deletions

2
.envrc Normal file
View file

@ -0,0 +1,2 @@
dotenv;
use flake;

1
.gitignore vendored
View file

@ -25,3 +25,4 @@ go.work.sum
# env file # env file
.env .env
.direnv/

View file

@ -1,3 +1,33 @@
# ndi # ndi
iNDexed Image format - consider itself lucky. iNDexed Image format - consider itself lucky.
This is the Go reference for `ndi` v1.
## Format
### Header
magic string: `ndi1`
image width: uint16
LUT/palette size: uint8 (maximum of 255 colors)
zero pad until 12 bytes
`ndi1 WWL0 0000`
### LUT (lookup table) / palette
3x uint8 in RGB order, per LUT index.
zero pad: 32 bits
### Pixels
a uint8 per pixel, referencing the color within the LUT, zero indexed.
## Decoding Quirks
Pixels should modulo their value with LUT size. A pixel referencing index 0xEF when only 10 colors exist in the LUT, `0xEF % 10` is the correct interpretation.

60
cmd/ndiconv/main.go Normal file
View file

@ -0,0 +1,60 @@
package main
import (
"image"
"image/gif"
"image/jpeg"
"image/png"
"log"
"os"
"path"
"git.sapphic.engineer/noe/esi"
"git.sapphic.engineer/noe/ndi"
)
func main() {
convPath := os.Args[1]
outPath := os.Args[2]
fin, err := os.Open(convPath)
if err != nil {
log.Fatalln(err)
}
fout, err := os.OpenFile(outPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
if err != nil {
log.Fatalln(err)
}
img, _, err := image.Decode(fin)
if err != nil {
log.Fatalln(err)
}
fin.Close()
ext := path.Ext(outPath)
switch ext[1:] {
case "ndi":
err = ndi.Encode(fout, img, nil)
case "esi":
err = esi.Encode(fout, img)
case "png":
err = png.Encode(fout, img)
case "jpg":
fallthrough
case "jpeg":
err = jpeg.Encode(fout, img, nil)
case "gif":
err = gif.Encode(fout, img, nil)
default:
log.Fatalf("extension %s unknown", ext)
}
if err != nil {
log.Fatalln(err)
}
fout.Close()
}

58
flake.lock generated Normal file
View file

@ -0,0 +1,58 @@
{
"nodes": {
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1730504689,
"narHash": "sha256-hgmguH29K2fvs9szpq2r3pz2/8cJd2LPS+b4tfNFCwE=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "506278e768c2a08bec68eb62932193e341f55c90",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1730531603,
"narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7ffd9ae656aec493492b44d0ddfb28e79a1ea25d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1730504152,
"narHash": "sha256-lXvH/vOfb4aGYyvFmZK/HlsNsr/0CVWlwYvo2rxJk3s=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/cc2f28000298e1269cea6612cd06ec9979dd5d7f.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/cc2f28000298e1269cea6612cd06ec9979dd5d7f.tar.gz"
}
},
"root": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

15
flake.nix Normal file
View file

@ -0,0 +1,15 @@
{
description = "saerro";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
};
outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } {
systems = [ "x86_64-linux" "aarch64-linux" ];
perSystem = { config, self', pkgs, lib, system, ... }: {
devShells.default = import ./shell.nix { inherit pkgs; };
};
};
}

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module git.sapphic.engineer/noe/ndi
go 1.23.2

219
ndi.go Normal file
View file

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

32
shell.nix Normal file
View file

@ -0,0 +1,32 @@
{ pkgs ? import <nixpkgs> {} }: let
podmanSetupScript = let
registriesConf = pkgs.writeText "registries.conf" ''
[registries.search]
registries = ['docker.io']
[registries.block]
registries = []
'';
in pkgs.writeScript "podman-setup" ''
#!${pkgs.runtimeShell}
# Dont overwrite customised configuration
if ! test -f ~/.config/containers/policy.json; then
install -Dm555 ${pkgs.skopeo.src}/default-policy.json ~/.config/containers/policy.json
fi
if ! test -f ~/.config/containers/registries.conf; then
install -Dm555 ${registriesConf} ~/.config/containers/registries.conf
fi
'';
# Provides a fake "docker" binary mapping to podman
dockerCompat = pkgs.runCommandNoCC "docker-podman-compat" {} ''
mkdir -p $out/bin
ln -s ${pkgs.podman}/bin/podman $out/bin/docker
'';
in pkgs.mkShell {
buildInputs = with pkgs; [
go
just
docker-compose
sqlite
];
}