diff --git a/README.md b/README.md new file mode 100644 index 0000000..4cb38e7 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +## goi - The “Quite OK Image” format encoder / decoder for Go. + +Original: https://github.com/phoboslab/qoi + +# LICENSE + +MIT \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e4064c2 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/goi + +go 1.17 diff --git a/goi.go b/goi.go new file mode 100644 index 0000000..2ab97aa --- /dev/null +++ b/goi.go @@ -0,0 +1,279 @@ +package goi + +import ( + "bytes" + "encoding/binary" + "errors" + "image" + "image/color" + "io" +) + +const ( + qoiIndex = 0x00 // 00xxxxxx + qoiRun8 = 0x40 // 010xxxxx + qoiRun16 = 0x60 // 011xxxxx + qoiDiff8 = 0x80 // 10xxxxxx + qoiDiff16 = 0xc0 // 110xxxxx + qoiDiff24 = 0xe0 // 1110xxxx + qoiColor = 0xf0 // 1111xxxx + qoiMask2 = 0xc0 // 11000000 + qoiMask3 = 0xe0 // 11100000 + qoiMask4 = 0xf0 // 11110000 +) + +func colorHash(c color.RGBA) byte { + return c.R ^ c.G ^ c.B ^ c.A +} + +func rgbaColor(c color.Color) color.RGBA { + if rgba, ok := c.(color.RGBA); ok { + return rgba + } else if nrgba, ok := c.(color.NRGBA); ok { + return color.RGBA{ + R: nrgba.R, + G: nrgba.G, + B: nrgba.B, + A: nrgba.A, + } + } + r, g, b, a := c.RGBA() + return color.RGBA{ + R: uint8(r << 8), + G: uint8(g << 8), + B: uint8(b << 8), + A: uint8(a << 8), + } +} + +const ( + magic = "qoif" + headerSize = 12 + padding = 4 +) + +type errWriter struct { + err error + wr io.Writer +} + +func (w *errWriter) Write(b byte) { + if w.err != nil { + return + } + w.err = binary.Write(w.wr, binary.BigEndian, b) +} + +func cond3(cond int, then int, els int) int { + if cond != 0 { + return then + } + return els +} + +func Encode(w io.Writer, m image.Image) error { + data := bytes.NewBuffer(nil) + wr := errWriter{wr: data} + + var index [64]color.RGBA + run := 0 + pxPrev := color.RGBA{A: 255} + + for y := m.Bounds().Min.Y; y < m.Bounds().Max.Y; y++ { + for x := m.Bounds().Min.X; x < m.Bounds().Max.X; x++ { + px := rgbaColor(m.At(x, y)) + if px == pxPrev { + run++ + } + + last := (x == m.Bounds().Max.X-1) && (y == m.Bounds().Max.Y-1) + if run > 0 && (run == 0x2020 || px != pxPrev || last) { + if run < 33 { + run -= 1 + wr.Write(byte(qoiRun8 | run)) + } else { + run -= 33 + wr.Write(byte(qoiRun16 | run>>8)) + wr.Write(byte(run)) + } + run = 0 + } + + if px != pxPrev { + indexPos := colorHash(px) % 64 + + if index[indexPos] == px { + wr.Write(qoiIndex | indexPos) + } else { + index[indexPos] = px + + var vr int = int(px.R) - int(pxPrev.R) + var vg int = int(px.G) - int(pxPrev.G) + var vb int = int(px.B) - int(pxPrev.B) + var va int = int(px.A) - int(pxPrev.A) + + if vr > -16 && vr < 17 && vg > -16 && vg < 17 && + vb > -16 && vb < 17 && va > -16 && va < 17 { + if va == 0 && vr > -2 && vr < 3 && + vg > -2 && vg < 3 && vb > -2 && vb < 3 { + wr.Write(byte(qoiDiff8 | ((vr + 1) << 4) | (vg+1)<<2 | (vb + 1))) + } else if va == 0 && vr > -16 && vr < 17 && + vg > -8 && vg < 9 && vb > -8 && vb < 9 { + wr.Write(byte(qoiDiff16 | (vr + 15))) + wr.Write(byte(((vg + 7) << 4) | (vb + 7))) + } else { + wr.Write(byte(qoiDiff24 | ((vr + 15) >> 1))) + wr.Write(byte(((vr + 15) << 7) | ((vg + 15) << 2) | ((vb + 15) >> 3))) + wr.Write(byte(((vb + 15) << 5) | (va + 15))) + } + } else { + wr.Write(byte(qoiColor | (cond3(vr, 8, 0)) | (cond3(vg, 4, 0)) | (cond3(vb, 2, 0)) | (cond3(va, 1, 0)))) + if vr != 0 { + wr.Write(px.R) + } + if vg != 0 { + wr.Write(px.G) + } + if vb != 0 { + wr.Write(px.B) + } + if va != 0 { + wr.Write(px.A) + } + } + } + } + pxPrev = px + } + } + + for i := 0; i < padding; i++ { + wr.Write(0) + } + + dataLen := len(data.Bytes()) + + if err := binary.Write(w, binary.BigEndian, []byte(magic)); err != nil { + return err + } + if err := binary.Write(w, binary.BigEndian, uint16(m.Bounds().Dx())); err != nil { + return err + } + if err := binary.Write(w, binary.BigEndian, uint16(m.Bounds().Dy())); err != nil { + return err + } + if err := binary.Write(w, binary.BigEndian, uint32(dataLen)); err != nil { + return err + } + if _, err := io.Copy(w, bytes.NewReader(data.Bytes())); err != nil { + return err + } + + return nil +} + +var errInvalidHeader = errors.New("invalid header") +var errInvalidFileSize = errors.New("invalid file size") + +func Decode(r io.Reader) (image.Image, error) { + mgc := make([]byte, 4) + if err := binary.Read(r, binary.BigEndian, &mgc); err != nil { + return nil, err + } + var w, h uint16 + if err := binary.Read(r, binary.BigEndian, &w); err != nil { + return nil, err + } + if err := binary.Read(r, binary.BigEndian, &h); err != nil { + return nil, err + } + + var dataLen uint32 + if err := binary.Read(r, binary.BigEndian, &dataLen); err != nil { + return nil, err + } + + if !bytes.Equal(mgc, []byte(magic)) { + return nil, errInvalidHeader + } + + data, err := io.ReadAll(r) + if err != nil { + return nil, err + } + if int(dataLen) != len(data) { + return nil, errInvalidFileSize + } + + m := image.NewRGBA(image.Rectangle{ + Max: image.Point{X: int(w), Y: int(h)}, + }) + + pxLen := int(w) * int(h) * 4 + px := color.RGBA{A: 255} + var index [64]color.RGBA + + p := 0 + run := 0 + chunksLen := dataLen - padding + + for pxPos := 0; pxPos < int(pxLen); pxPos += 4 { + if run > 0 { + run-- + } else if p < int(chunksLen) { + b1 := data[p] + p++ + + if (b1 & qoiMask2) == qoiIndex { + px = index[b1^qoiIndex] + } else if (b1 & qoiMask3) == qoiRun8 { + run = int(b1 & 0x1f) + } else if (b1 & qoiMask3) == qoiRun16 { + b2 := int(data[p]) + p++ + run = ((int(b1&0x1f) << 8) | (b2)) + 32 + } else if (b1 & qoiMask2) == qoiDiff8 { + px.R += ((b1 >> 4) & 0x03) - 1 + px.G += ((b1 >> 2) & 0x03) - 1 + px.B += (b1 & 0x03) - 1 + } else if (b1 & qoiMask3) == qoiDiff16 { + b2 := int(data[p]) + p++ + px.R += byte(b1&0x1f) - 15 + px.G += byte(b2>>4) - 7 + px.B += byte(b2&0x0f) - 7 + } else if (b1 & qoiMask4) == qoiDiff24 { + b2 := int(data[p]) + p++ + b3 := int(data[p]) + p++ + px.R += byte((int(b1&0x0f)<<1)|(b2>>7)) - 15 + px.G += byte((b2&0x7c)>>2) - 15 + px.B += byte(((b2&0x03)<<3)|((b3&0xe0)>>5)) - 15 + px.A += byte(b3&0x1f) - 15 + } else if (b1 & qoiMask4) == qoiColor { + if b1&8 != 0 { + px.R = data[p] + p++ + } + if b1&4 != 0 { + px.G = data[p] + p++ + } + if b1&2 != 0 { + px.B = data[p] + p++ + } + if b1&1 != 0 { + px.A = data[p] + p++ + } + } + + index[colorHash(px)%64] = px + } + + m.Pix[pxPos], m.Pix[pxPos+1], m.Pix[pxPos+2], m.Pix[pxPos+3] = px.R, px.G, px.B, px.A + } + return m, nil +} diff --git a/goi.png b/goi.png new file mode 100644 index 0000000..acc0599 Binary files /dev/null and b/goi.png differ diff --git a/goi_test.go b/goi_test.go new file mode 100644 index 0000000..550b213 --- /dev/null +++ b/goi_test.go @@ -0,0 +1,45 @@ +package goi + +import ( + "bytes" + "image/png" + "os" + "testing" +) + +func TestEncodeDecode(t *testing.T) { + f, err := os.Open("goi.png") + if err != nil { + t.Fatal(err) + } + img, err := png.Decode(f) + if err != nil { + t.Fatal(err) + } + + wr := bytes.NewBuffer(nil) + if err := Encode(wr, img); err != nil { + t.Fatal(err) + } + + img2, err := Decode(bytes.NewReader(wr.Bytes())) + if err != nil { + t.Fatal(err) + } + + if img.Bounds() != img2.Bounds() { + t.Fatal("bound mismatch") + } + + i := 0 + for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { + for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { + r1, g1, b1, a1 := img.At(x, y).RGBA() + r2, g2, b2, a2 := img2.At(x, y).RGBA() + if r1 != r2 || g1 != g2 || b1 != b2 || a1 != a2 { + t.Fatal("At mismatch", i, x, y, img.At(x, y), img2.At(x, y)) + } + i++ + } + } +}