Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
1
go.mod
1
go.mod
@ -1,4 +1,3 @@
|
|||||||
// Deprecated: use smariot.com/color/noklaba instead.
|
|
||||||
module smariot.com/oklab
|
module smariot.com/oklab
|
||||||
|
|
||||||
go 1.22.1
|
go 1.22.1
|
||||||
|
164
image.go
164
image.go
@ -1,164 +0,0 @@
|
|||||||
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)
|
|
||||||
)
|
|
245
image_test.go
245
image_test.go
@ -1,245 +0,0 @@
|
|||||||
package oklab
|
|
||||||
|
|
||||||
import (
|
|
||||||
"image"
|
|
||||||
"image/color"
|
|
||||||
"image/draw"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewImage(t *testing.T) {
|
|
||||||
r := image.Rect(13, 21, 34, 55)
|
|
||||||
img := NewImage(r)
|
|
||||||
|
|
||||||
if img.Rect != r {
|
|
||||||
t.Errorf("img.Rect = %v, want %v", img.Rect, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := img.Bounds(); got != r {
|
|
||||||
t.Errorf("img.Bounds() = %v, want %v", got, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := img.ColorModel(); got != Model {
|
|
||||||
t.Errorf("img.ColorModel() = %v, want %v", got, Model)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := img.Stride, r.Dx()*bytesPerPixel; got != want {
|
|
||||||
t.Errorf("img.Stride = %v, want %v", got, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := len(img.Pix), (r.Dy()-1)*img.Stride+r.Dx()*bytesPerPixel; got != want {
|
|
||||||
t.Errorf("len(img.Pix) = %v, want %v", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testColor(x, y int) Color {
|
|
||||||
return Color{
|
|
||||||
float64(x),
|
|
||||||
float64(y),
|
|
||||||
float64(x + y),
|
|
||||||
float64(x ^ y),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mod(a int, m int) int {
|
|
||||||
a = a % m
|
|
||||||
if a < 0 {
|
|
||||||
// fucking intel.
|
|
||||||
a += m
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRGBA64Color(x, y int) color.RGBA64 {
|
|
||||||
a := mod(x^y, 0xffff)
|
|
||||||
|
|
||||||
return color.RGBA64{
|
|
||||||
uint16(mod(x, a+1)),
|
|
||||||
uint16(mod(y, a+1)),
|
|
||||||
uint16(mod(x+y, a+1)),
|
|
||||||
uint16(a),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImage_Set(t *testing.T) {
|
|
||||||
r := image.Rect(13, 21, 34, 55)
|
|
||||||
img := NewImage(r)
|
|
||||||
|
|
||||||
// intentionally invoking SetOkLab beyond the boundaries of the image, here.
|
|
||||||
for y := r.Min.Y - 1; y <= r.Max.Y; y++ {
|
|
||||||
for x := r.Min.X - 1; x <= r.Max.X; x++ {
|
|
||||||
img.Set(x, y, testColor(x, y))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for y := r.Min.Y - 1; y <= r.Max.Y; y++ {
|
|
||||||
for x := r.Min.X - 1; x <= r.Max.X; x++ {
|
|
||||||
want := Color{}
|
|
||||||
|
|
||||||
if image.Pt(x, y).In(r) {
|
|
||||||
want = testColor(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
got := img.At(x, y).(Color)
|
|
||||||
|
|
||||||
if got != want {
|
|
||||||
t.Errorf("img.At(%d, %d) = %v, want %v", x, y, got, want)
|
|
||||||
// stop at the first error, please.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImage_SetRGBA64(t *testing.T) {
|
|
||||||
r := image.Rect(13, 21, 34, 55)
|
|
||||||
img := NewImage(r)
|
|
||||||
|
|
||||||
// intentionally invoking SetOkLab beyond the boundaries of the image, here.
|
|
||||||
for y := r.Min.Y - 1; y <= r.Max.Y; y++ {
|
|
||||||
for x := r.Min.X - 1; x <= r.Max.X; x++ {
|
|
||||||
img.SetRGBA64(x, y, testRGBA64Color(x, y))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for y := r.Min.Y - 1; y <= r.Max.Y; y++ {
|
|
||||||
for x := r.Min.X - 1; x <= r.Max.X; x++ {
|
|
||||||
want := color.RGBA64{}
|
|
||||||
|
|
||||||
if image.Pt(x, y).In(r) {
|
|
||||||
want = testRGBA64Color(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
got := img.RGBA64At(x, y)
|
|
||||||
|
|
||||||
if got != want {
|
|
||||||
t.Errorf("img.RGBA64At(%d, %d) = %v, want %v", x, y, got, want)
|
|
||||||
// stop at the first error, please.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setPixels(img *Image) {
|
|
||||||
for y := img.Rect.Min.Y - 1; y <= img.Rect.Max.Y; y++ {
|
|
||||||
for x := img.Rect.Min.X - 1; x <= img.Rect.Max.X; x++ {
|
|
||||||
img.SetOkLab(x, y, testColor(x, y))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkPixels(t *testing.T, img *Image) {
|
|
||||||
for y := img.Rect.Min.Y - 1; y <= img.Rect.Max.Y; y++ {
|
|
||||||
for x := img.Rect.Min.X - 1; x <= img.Rect.Max.X; x++ {
|
|
||||||
want := Color{}
|
|
||||||
|
|
||||||
if image.Pt(x, y).In(img.Rect) {
|
|
||||||
want = testColor(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
got := img.OkLabAt(x, y)
|
|
||||||
|
|
||||||
if got != want {
|
|
||||||
t.Errorf("checkPixels: img.OkLabAt(%d, %d) = %v, want %v", x, y, got, want)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImage_SetOkLab(t *testing.T) {
|
|
||||||
r := image.Rect(13, 21, 34, 55)
|
|
||||||
img := NewImage(r)
|
|
||||||
|
|
||||||
setPixels(img)
|
|
||||||
checkPixels(t, img)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImage_SubImage(t *testing.T) {
|
|
||||||
r := image.Rect(13, 21, 34, 55)
|
|
||||||
img := NewImage(r)
|
|
||||||
|
|
||||||
setPixels(img)
|
|
||||||
|
|
||||||
for _, tt := range []struct {
|
|
||||||
name string
|
|
||||||
r image.Rectangle
|
|
||||||
}{
|
|
||||||
{"identity", r},
|
|
||||||
{"inset", r.Inset(1)},
|
|
||||||
{"outset", r.Inset(-1)},
|
|
||||||
{"left", r.Add(image.Pt(-1, 0))},
|
|
||||||
{"right", r.Add(image.Pt(1, 0))},
|
|
||||||
{"up", r.Add(image.Pt(0, -1))},
|
|
||||||
{"down", r.Add(image.Pt(0, 1))},
|
|
||||||
{"outside", image.Rect(-100, -100, 1, 1)},
|
|
||||||
} {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
subImage := img.SubImage(tt.r)
|
|
||||||
|
|
||||||
if got, want := subImage.Rect, img.Rect.Intersect(tt.r); got != want {
|
|
||||||
t.Errorf("subImage.Rect = %v, want %v", got, want)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := subImage.Stride, subImage.Rect.Dx()*bytesPerPixel; got < want {
|
|
||||||
t.Errorf("subImage.Stride = %v, want >= %v", got, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := len(subImage.Pix), (subImage.Rect.Dy()-1)*subImage.Stride+subImage.Rect.Dx()*bytesPerPixel; got != want {
|
|
||||||
t.Errorf("len(img.Pix) = %v, want %v", got, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
checkPixels(t, subImage)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImage_Opaque(t *testing.T) {
|
|
||||||
t.Run("empty", func(t *testing.T) {
|
|
||||||
// an empty image is opaque by virtue of not containing any pixels, opaque or otherwise.
|
|
||||||
got, want := NewImage(image.Rect(0, 0, 0, 0)).Opaque(), true
|
|
||||||
if got != want {
|
|
||||||
t.Errorf("Opaque() = %v, want %v", got, want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("non-empty", func(t *testing.T) {
|
|
||||||
img := NewImage(image.Rect(13, 21, 34, 55))
|
|
||||||
|
|
||||||
// the pixels of a non-empty image will have an alpha value of zero,
|
|
||||||
// make it completely transparent.
|
|
||||||
got, want := img.Opaque(), false
|
|
||||||
if got != want {
|
|
||||||
t.Errorf("img.Opaque() = %v, want %v", got, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("explicitly transparent", func(t *testing.T) {
|
|
||||||
// if we explicitly set every pixel to be transparent, the result should be the same.
|
|
||||||
draw.Draw(img, img.Rect, image.Transparent, image.Point{}, draw.Src)
|
|
||||||
got, want := img.Opaque(), false
|
|
||||||
|
|
||||||
if got != want {
|
|
||||||
t.Errorf("img.Opaque() = %v, want %v", got, want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("opaque", func(t *testing.T) {
|
|
||||||
// make every pixel opaque.
|
|
||||||
draw.Draw(img, img.Rect, image.Opaque, image.Point{}, draw.Src)
|
|
||||||
got, want := img.Opaque(), true
|
|
||||||
if got != want {
|
|
||||||
t.Errorf("img.Opaque() = %v, want %v", got, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("except for a single transparent pixel", func(t *testing.T) {
|
|
||||||
img.Set(14, 23, color.Transparent)
|
|
||||||
got, want := img.Opaque(), false
|
|
||||||
if got != want {
|
|
||||||
t.Errorf("img.Opaque() = %v, want %v", got, want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
5
oklab.go
5
oklab.go
@ -194,8 +194,5 @@ var (
|
|||||||
Model = color.ModelFunc(okLabModel)
|
Model = color.ModelFunc(okLabModel)
|
||||||
|
|
||||||
// Type assertions.
|
// Type assertions.
|
||||||
_ interface {
|
_ NRGBAColor = Color{}
|
||||||
color.Color
|
|
||||||
NRGBAColor
|
|
||||||
} = Color{}
|
|
||||||
)
|
)
|
||||||
|
@ -76,18 +76,6 @@ func fixedNRGBA64Model(c color.Color) color.Color {
|
|||||||
return color.NRGBA64Model.Convert(c)
|
return color.NRGBA64Model.Convert(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
type testNRGBAColor struct {
|
|
||||||
color.NRGBA64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c testNRGBAColor) NRGBA() (r, g, b, a uint32) {
|
|
||||||
return uint32(c.R), uint32(c.G), uint32(c.B), uint32(c.A)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testNRGBAModel(c color.Color) color.Color {
|
|
||||||
return testNRGBAColor{fixedNRGBA64Model(c).(color.NRGBA64)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_Model(t *testing.T) {
|
func Test_Model(t *testing.T) {
|
||||||
// test to make sure we can reproduce some test colors.
|
// test to make sure we can reproduce some test colors.
|
||||||
// In particular, I want to make sure the colors can be recovered from transparent NRGBA and NRGBA64.
|
// In particular, I want to make sure the colors can be recovered from transparent NRGBA and NRGBA64.
|
||||||
@ -148,13 +136,6 @@ func Test_Model(t *testing.T) {
|
|||||||
"RGBA: invisible nothing",
|
"RGBA: invisible nothing",
|
||||||
color.RGBA{R: 0x0, G: 0x0, B: 0x0, A: 0x0},
|
color.RGBA{R: 0x0, G: 0x0, B: 0x0, A: 0x0},
|
||||||
color.RGBAModel,
|
color.RGBAModel,
|
||||||
}, {
|
|
||||||
// test the handling of the NRGBAColor interface; as
|
|
||||||
// all the types that support it have special handling
|
|
||||||
// that would take precedence.
|
|
||||||
"testNRGBAColor: invisible Floral White",
|
|
||||||
testNRGBAColor{color.NRGBA64{R: 0xff, G: 0xfa, B: 0xf0, A: 0x00}},
|
|
||||||
color.ModelFunc(testNRGBAModel),
|
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user