165 lines
4.2 KiB
Go
165 lines
4.2 KiB
Go
package oklab
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"image"
|
|
"image/color"
|
|
"image/draw"
|
|
"math"
|
|
)
|
|
|
|
// Not many images can say they have 256-bit color;
|
|
// I've definitely gone overboard with this.
|
|
const bytesPerPixel = 8 * 4
|
|
|
|
// Image represents an image that encodes its pixels using OKLab color.
|
|
//
|
|
// Each pixel is 32 bytes in size, encoding the Lightness, ChromaA, ChromaB, and Alpha components
|
|
// of each pixel in that order as big-endian float64 values.
|
|
type Image struct {
|
|
Pix []uint8
|
|
Stride int
|
|
Rect image.Rectangle
|
|
}
|
|
|
|
// ColorModel implements the [image.Image] interface.
|
|
func (p *Image) ColorModel() color.Model {
|
|
return Model
|
|
}
|
|
|
|
// Bounds implements the [image.Image] interface.
|
|
func (p *Image) Bounds() image.Rectangle {
|
|
return p.Rect
|
|
}
|
|
|
|
// At implements the [image.Image] interface.
|
|
func (p *Image) At(x, y int) color.Color {
|
|
return p.OkLabAt(x, y)
|
|
}
|
|
|
|
// RGBA64At implements the [image.RGBA64Image] interface.
|
|
func (p *Image) RGBA64At(x, y int) color.RGBA64 {
|
|
r, g, b, a := p.OkLabAt(x, y).RGBA()
|
|
return color.RGBA64{uint16(r), uint16(g), uint16(b), uint16(a)}
|
|
}
|
|
|
|
// OkLabAt decodes the pixel at the given coordinates into an OkLab color.
|
|
//
|
|
// The zero value is returned for coordinates outside of the image.
|
|
func (p *Image) OkLabAt(x, y int) Color {
|
|
if !image.Pt(x, y).In(p.Rect) {
|
|
return Color{}
|
|
}
|
|
|
|
i := p.PixOffset(x, y)
|
|
in := p.Pix[i : i+bytesPerPixel]
|
|
|
|
return Color{
|
|
Lightness: math.Float64frombits(binary.BigEndian.Uint64(in[0:8])),
|
|
ChromaA: math.Float64frombits(binary.BigEndian.Uint64(in[8:16])),
|
|
ChromaB: math.Float64frombits(binary.BigEndian.Uint64(in[16:24])),
|
|
A: math.Float64frombits(binary.BigEndian.Uint64(in[24:32])),
|
|
}
|
|
}
|
|
|
|
// PixOffset returns the offset into the [Pix] slice a pixel begins.
|
|
//
|
|
// The given coordinates must be inside the image.
|
|
func (p *Image) PixOffset(x, y int) int {
|
|
return (y-p.Rect.Min.Y)*p.Stride + (x-p.Rect.Min.X)*bytesPerPixel
|
|
}
|
|
|
|
// Set implements the [draw.Image] interface.
|
|
func (p *Image) Set(x, y int, c color.Color) {
|
|
if !image.Pt(x, y).In(p.Rect) {
|
|
return
|
|
}
|
|
|
|
p.setOkLab(x, y, FromColor(c))
|
|
}
|
|
|
|
// Set implements the [draw.RGBA64Image] interface.
|
|
func (p *Image) SetRGBA64(x, y int, c color.RGBA64) {
|
|
if !image.Pt(x, y).In(p.Rect) {
|
|
return
|
|
}
|
|
|
|
p.setOkLab(x, y, FromRGBA(uint32(c.R), uint32(c.G), uint32(c.B), uint32(c.A)))
|
|
}
|
|
|
|
func (p *Image) setOkLab(x, y int, c Color) {
|
|
i := p.PixOffset(x, y)
|
|
out := p.Pix[i : i+bytesPerPixel]
|
|
|
|
binary.BigEndian.PutUint64(out[0:8], math.Float64bits(c.Lightness))
|
|
binary.BigEndian.PutUint64(out[8:16], math.Float64bits(c.ChromaA))
|
|
binary.BigEndian.PutUint64(out[16:24], math.Float64bits(c.ChromaB))
|
|
binary.BigEndian.PutUint64(out[24:32], math.Float64bits(c.A))
|
|
}
|
|
|
|
// SetOkLab encodes the given color into the pixel at the given coordinates.
|
|
//
|
|
// Does nothing if the coordinates are outside the image.
|
|
func (p *Image) SetOkLab(x, y int, c Color) {
|
|
if !image.Pt(x, y).In(p.Rect) {
|
|
return
|
|
}
|
|
|
|
p.setOkLab(x, y, c)
|
|
}
|
|
|
|
// SubImage returns a new [Image] that shares the same underlying pixel data.
|
|
func (p *Image) SubImage(r image.Rectangle) *Image {
|
|
r = r.Intersect(p.Rect)
|
|
|
|
if r.Empty() {
|
|
return &Image{}
|
|
}
|
|
|
|
start := p.PixOffset(r.Min.X, r.Min.Y)
|
|
end := p.PixOffset(r.Max.X-1, r.Max.Y-1) + bytesPerPixel
|
|
|
|
return &Image{
|
|
Pix: p.Pix[start:end:end],
|
|
Stride: p.Stride,
|
|
Rect: r,
|
|
}
|
|
}
|
|
|
|
// Opaque returns true if every pixel in the image would have an alpha value of 0xffff when
|
|
// converted to [color.RGBA64].
|
|
func (p *Image) Opaque() bool {
|
|
w, h := p.Rect.Dx(), p.Rect.Dy()
|
|
for y := 0; y < h; y++ {
|
|
row := p.Pix[y*p.Stride : y*p.Stride+w*bytesPerPixel]
|
|
for i, stop := 0, w*bytesPerPixel; i < stop; i += bytesPerPixel {
|
|
// we consider a pixel opaque if the NRGBA/RGBA methods
|
|
// would have returned 0xffff.
|
|
if uint32(math.Float64frombits(binary.BigEndian.Uint64(row[i+24:i+32]))*0xffff+0.5) != 0xffff {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// NewImage returns a new [Image] of the specified size.
|
|
func NewImage(r image.Rectangle) *Image {
|
|
return &Image{
|
|
Pix: make([]uint8, r.Dx()*r.Dy()*bytesPerPixel),
|
|
Stride: r.Dx() * bytesPerPixel,
|
|
Rect: r,
|
|
}
|
|
}
|
|
|
|
var (
|
|
// type assertions
|
|
_ interface {
|
|
image.Image
|
|
draw.Image
|
|
image.RGBA64Image
|
|
draw.RGBA64Image
|
|
} = (*Image)(nil)
|
|
)
|