Created an Image type.
This commit is contained in:
		
							
								
								
									
										164
									
								
								image.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								image.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,164 @@ | |||||||
|  | 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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								image_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,245 @@ | |||||||
|  | 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) | ||||||
|  | 				} | ||||||
|  | 			}) | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user