224 lines
4.9 KiB
Go
224 lines
4.9 KiB
Go
package esi
|
|
|
|
/*
|
|
> Extremely Simple Image file format <
|
|
>------------------------------------------------------------------------------------------<
|
|
> Designed for databending or glitching
|
|
> Has very little fancy features that could cause problems with decoding
|
|
> Decoder is designed to assume, without any penalties if it assumes wrong
|
|
> Width parameter is assumed to be the square root of the length of the data, rounded up
|
|
> Color format is assumed to be 8 bit color
|
|
> Can support grayscale or color in a variety of bit depths
|
|
> There is no data between or around pixels, or at the end of the file
|
|
> It is literally a sequence of bits after the minimal header
|
|
> This should prevent any issues with databending
|
|
> Header is encoded with 16 bits for width, 1 indexed, for a maximum width of 65536 pixels
|
|
> All data is big-endian encoded
|
|
> This is followed by the following 5 bits which is followed by 11 ignored bits:
|
|
> | 0
|
|
> | 0000 1 bit black and white
|
|
> | #### 4 bits encoding bit depth, 1 indexed
|
|
> -------------------------------
|
|
> | 1
|
|
> | #### 4 bits encoding bit depth per channel, 1 indexed
|
|
>
|
|
> Sample header:
|
|
> | 01100101 01110011 01101001 00110001 ( esi1 in ascii binary )
|
|
> | 00000000 00010000 10100000 00000000
|
|
>
|
|
> | 00000000
|
|
> | 00010000 width of 32
|
|
> | 1 color
|
|
> | 0100 8 bit depth
|
|
> | 000 00000000 ignored bits
|
|
> | ( to ease databending the entire image )
|
|
> | ( without affecting the header )
|
|
*/
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"image"
|
|
"image/color"
|
|
"io"
|
|
)
|
|
|
|
var (
|
|
errImageInvalid = errors.New("esi: image is invalid")
|
|
errNotESI = errors.New("esi: not an esi")
|
|
)
|
|
|
|
var emptyBytes = 4
|
|
|
|
func init() {
|
|
image.RegisterFormat("esi", "esi1", Decode, DecodeConfig)
|
|
}
|
|
|
|
func Decode(r io.Reader) (o image.Image, err error) {
|
|
buf := bytes.Buffer{}
|
|
io.Copy(&buf, r)
|
|
|
|
cfgBuf := bytes.NewBuffer(buf.Bytes())
|
|
cfg, err := DecodeConfig(cfgBuf)
|
|
if err != nil {
|
|
return o, err
|
|
}
|
|
|
|
bounds := image.Rect(0, 0, cfg.Width, cfg.Height)
|
|
|
|
img := image.NewRGBA(bounds)
|
|
|
|
buf.Next(8 + emptyBytes)
|
|
|
|
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
|
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
|
|
|
r, e1 := buf.ReadByte()
|
|
g, e2 := buf.ReadByte()
|
|
b, e3 := buf.ReadByte()
|
|
|
|
for _, err := range []error{e1, e2, e3} {
|
|
// log.Println(err)
|
|
if err != nil && err != io.EOF {
|
|
return o, err
|
|
}
|
|
}
|
|
|
|
c := color.RGBA{
|
|
R: r,
|
|
G: g,
|
|
B: b,
|
|
A: 255,
|
|
}
|
|
|
|
img.SetRGBA(x, y, c)
|
|
// log.Println(x, y, c)
|
|
// log.Println(img.At(x, y).RGBA())
|
|
}
|
|
}
|
|
|
|
return img, nil
|
|
}
|
|
|
|
func DecodeConfig(r io.Reader) (cfg image.Config, err error) {
|
|
// pull first 8 bytes out...
|
|
buf := bytes.Buffer{}
|
|
if err != nil {
|
|
return cfg, err
|
|
}
|
|
|
|
buf.ReadFrom(r)
|
|
|
|
// check first 4 == esi1
|
|
magic := buf.Next(4)
|
|
if !bytes.Equal(magic, []byte("esi1")) {
|
|
return cfg, errNotESI
|
|
}
|
|
|
|
// read headers
|
|
header := buf.Next(4)
|
|
w := binary.BigEndian.Uint16(header[0:2]) // width is first two bytes (16 bit)
|
|
cfg.Width = int(w)
|
|
|
|
cfg.ColorModel = GetColorModel(header[2])
|
|
|
|
// calculate length as a product of width / length of reader
|
|
|
|
size := buf.Len()
|
|
cfg.Height = size / 3 / cfg.Width
|
|
|
|
// log.Println("size w/o headers", size)
|
|
// log.Println("height:", cfg.Height)
|
|
// log.Println("width:", cfg.Width)
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func Encode(w io.Writer, img image.Image) error {
|
|
bounds := img.Bounds()
|
|
|
|
_, err := w.Write([]byte("esi1"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = binary.Write(w, binary.BigEndian, uint16(bounds.Max.X))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cm := img.ColorModel()
|
|
cc := true
|
|
cw := 8
|
|
|
|
switch cm {
|
|
case color.GrayModel:
|
|
fallthrough
|
|
case color.Gray16Model:
|
|
cc = false
|
|
}
|
|
|
|
switch cm {
|
|
case color.RGBA64Model:
|
|
fallthrough
|
|
case color.Gray16Model:
|
|
cw = 16
|
|
}
|
|
|
|
configByte := byte(0)
|
|
|
|
if cc {
|
|
configByte |= 128
|
|
}
|
|
|
|
if cw == 16 {
|
|
configByte |= 64
|
|
} else {
|
|
configByte |= 32
|
|
}
|
|
|
|
cfgpad := make([]byte, 2+emptyBytes) // 1 byte for config, 1 empty byte, and a number of empty bytes.
|
|
cfgpad[0] = configByte
|
|
_, err = w.Write(cfgpad)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for y := 0; y < bounds.Max.Y; y++ {
|
|
for x := 0; x < bounds.Max.X; x++ {
|
|
r, g, b, _ := img.At(x, y).RGBA()
|
|
_, err = w.Write([]byte{byte(r / 257), byte(g / 257), byte(b / 257)})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetColorModel takes the encoding data byte of an ESI header,
|
|
// and outputs the corresponding color.Model
|
|
func GetColorModel(b byte) (m color.Model) {
|
|
cc := b&(1<<7)>>7 == 1 // color or not is first bit
|
|
depth := b &^ 128 >> 2 // unshift first bit, read next few
|
|
|
|
return color.RGBAModel
|
|
|
|
if cc {
|
|
if depth == 16 {
|
|
m = color.RGBA64Model
|
|
} else {
|
|
m = color.RGBAModel
|
|
}
|
|
} else {
|
|
if depth == 16 {
|
|
m = color.Gray16Model
|
|
} else {
|
|
m = color.GrayModel
|
|
}
|
|
}
|
|
|
|
return m
|
|
}
|