From 4512d06eb725586ce1173402d62554099db522b2 Mon Sep 17 00:00:00 2001 From: neguse Date: Sat, 27 Nov 2021 06:32:23 +0900 Subject: [PATCH] Initial commit. --- README.md | 7 ++ go.mod | 3 + goi.go | 279 ++++++++++++++++++++++++++++++++++++++++++++++++++++ goi.png | Bin 0 -> 3367 bytes goi_test.go | 45 +++++++++ 5 files changed, 334 insertions(+) create mode 100644 README.md create mode 100644 go.mod create mode 100644 goi.go create mode 100644 goi.png create mode 100644 goi_test.go 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 0000000000000000000000000000000000000000..acc05993b77335ba7f88ad9d849683eb47424d76 GIT binary patch literal 3367 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