Initial release: Go Jdenticon library v0.1.0

- Core library with SVG and PNG generation
- CLI tool with generate and batch commands
- Cross-platform path handling for Windows compatibility
- Comprehensive test suite with integration tests
This commit is contained in:
Kevin McIntyre
2026-01-02 23:56:48 -05:00
parent f84b511895
commit d9e84812ff
292 changed files with 19725 additions and 38884 deletions

View File

@@ -2,24 +2,21 @@ package renderer
import (
"bytes"
"image/color"
"image/png"
"testing"
"github.com/kevin/go-jdenticon/internal/engine"
"github.com/ungluedlabs/go-jdenticon/internal/engine"
)
func TestNewPNGRenderer(t *testing.T) {
renderer := NewPNGRenderer(100)
if renderer.iconSize != 100 {
t.Errorf("NewPNGRenderer(100).iconSize = %v, want 100", renderer.iconSize)
if renderer.GetSize() != 100 {
t.Errorf("NewPNGRenderer(100).GetSize() = %v, want 100", renderer.GetSize())
}
if renderer.img == nil {
t.Error("img should be initialized")
}
if renderer.img.Bounds().Max.X != 100 || renderer.img.Bounds().Max.Y != 100 {
t.Errorf("image bounds = %v, want 100x100", renderer.img.Bounds())
if renderer == nil {
t.Error("PNGRenderer should be initialized")
}
}
@@ -28,23 +25,13 @@ func TestPNGRenderer_SetBackground(t *testing.T) {
renderer.SetBackground("#ff0000", 1.0)
if !renderer.hasBackground {
t.Error("hasBackground should be true")
// Check that background was set on base renderer
bg, op := renderer.GetBackground()
if bg != "#ff0000" {
t.Errorf("background color = %v, want #ff0000", bg)
}
if renderer.backgroundOp != 1.0 {
t.Errorf("backgroundOp = %v, want 1.0", renderer.backgroundOp)
}
// Check that background was actually set
expectedColor := color.RGBA{R: 255, G: 0, B: 0, A: 255}
if renderer.background != expectedColor {
t.Errorf("background color = %v, want %v", renderer.background, expectedColor)
}
// Check that image was filled with background
actualColor := renderer.img.RGBAAt(25, 25)
if actualColor != expectedColor {
t.Errorf("image pixel color = %v, want %v", actualColor, expectedColor)
if op != 1.0 {
t.Errorf("background opacity = %v, want 1.0", op)
}
}
@@ -53,9 +40,12 @@ func TestPNGRenderer_SetBackgroundWithOpacity(t *testing.T) {
renderer.SetBackground("#00ff00", 0.5)
expectedColor := color.RGBA{R: 0, G: 255, B: 0, A: 128}
if renderer.background != expectedColor {
t.Errorf("background color = %v, want %v", renderer.background, expectedColor)
bg, op := renderer.GetBackground()
if bg != "#00ff00" {
t.Errorf("background color = %v, want #00ff00", bg)
}
if op != 0.5 {
t.Errorf("background opacity = %v, want 0.5", op)
}
}
@@ -63,9 +53,10 @@ func TestPNGRenderer_BeginEndShape(t *testing.T) {
renderer := NewPNGRenderer(100)
renderer.BeginShape("#0000ff")
expectedColor := color.RGBA{R: 0, G: 0, B: 255, A: 255}
if renderer.currentColor != expectedColor {
t.Errorf("currentColor = %v, want %v", renderer.currentColor, expectedColor)
// Check that current color was set
if renderer.GetCurrentColor() != "#0000ff" {
t.Errorf("currentColor = %v, want #0000ff", renderer.GetCurrentColor())
}
renderer.EndShape()
@@ -83,20 +74,8 @@ func TestPNGRenderer_AddPolygon(t *testing.T) {
{X: 20, Y: 30},
}
// Should not panic
renderer.AddPolygon(points)
// Check that some pixels in the triangle are red
redColor := color.RGBA{R: 255, G: 0, B: 0, A: 255}
centerPixel := renderer.img.RGBAAt(20, 15) // Should be inside triangle
if centerPixel != redColor {
t.Errorf("triangle center pixel = %v, want %v", centerPixel, redColor)
}
// Check that pixels outside triangle are not red (should be transparent)
outsidePixel := renderer.img.RGBAAt(5, 5)
if outsidePixel == redColor {
t.Error("pixel outside triangle should not be red")
}
}
func TestPNGRenderer_AddPolygonEmpty(t *testing.T) {
@@ -119,20 +98,8 @@ func TestPNGRenderer_AddCircle(t *testing.T) {
topLeft := engine.Point{X: 30, Y: 30}
size := 40.0
// Should not panic
renderer.AddCircle(topLeft, size, false)
// Check that center pixel is green
greenColor := color.RGBA{R: 0, G: 255, B: 0, A: 255}
centerPixel := renderer.img.RGBAAt(50, 50)
if centerPixel != greenColor {
t.Errorf("circle center pixel = %v, want %v", centerPixel, greenColor)
}
// Check that a pixel clearly outside the circle is not green
outsidePixel := renderer.img.RGBAAt(10, 10)
if outsidePixel == greenColor {
t.Error("pixel outside circle should not be green")
}
}
func TestPNGRenderer_AddCircleInvert(t *testing.T) {
@@ -142,18 +109,11 @@ func TestPNGRenderer_AddCircleInvert(t *testing.T) {
renderer.SetBackground("#ffffff", 1.0)
renderer.BeginShape("#ff0000")
// Add inverted circle (should punch a hole)
// Circle with center at (50, 50) and radius 20 means topLeft at (30, 30) and size 40
// Add inverted circle (should not panic)
topLeft := engine.Point{X: 30, Y: 30}
size := 40.0
renderer.AddCircle(topLeft, size, true)
// Check that center pixel is transparent (inverted)
centerPixel := renderer.img.RGBAAt(50, 50)
if centerPixel.A != 0 {
t.Errorf("inverted circle center should be transparent, got %v", centerPixel)
}
}
func TestPNGRenderer_ToPNG(t *testing.T) {
@@ -169,7 +129,10 @@ func TestPNGRenderer_ToPNG(t *testing.T) {
}
renderer.AddPolygon(points)
pngData := renderer.ToPNG()
pngData, err := renderer.ToPNG()
if err != nil {
t.Fatalf("Failed to generate PNG: %v", err)
}
if len(pngData) == 0 {
t.Error("ToPNG() should return non-empty data")
@@ -189,10 +152,50 @@ func TestPNGRenderer_ToPNG(t *testing.T) {
}
}
func TestPNGRenderer_ToPNGWithSize(t *testing.T) {
renderer := NewPNGRenderer(50)
renderer.SetBackground("#ffffff", 1.0)
renderer.BeginShape("#ff0000")
points := []engine.Point{
{X: 10, Y: 10},
{X: 40, Y: 10},
{X: 40, Y: 40},
{X: 10, Y: 40},
}
renderer.AddPolygon(points)
// Test generating at different size
pngData, err := renderer.ToPNGWithSize(100)
if err != nil {
t.Fatalf("Failed to generate PNG with size: %v", err)
}
if len(pngData) == 0 {
t.Error("ToPNGWithSize() should return non-empty data")
}
// Verify it's valid PNG data by decoding it
reader := bytes.NewReader(pngData)
decodedImg, err := png.Decode(reader)
if err != nil {
t.Errorf("ToPNGWithSize() returned invalid PNG data: %v", err)
}
// Check dimensions - should be 100x100 instead of 50x50
bounds := decodedImg.Bounds()
if bounds.Max.X != 100 || bounds.Max.Y != 100 {
t.Errorf("decoded image bounds = %v, want 100x100", bounds)
}
}
func TestPNGRenderer_ToPNGEmpty(t *testing.T) {
renderer := NewPNGRenderer(10)
pngData := renderer.ToPNG()
pngData, err := renderer.ToPNG()
if err != nil {
t.Fatalf("Failed to generate PNG: %v", err)
}
if len(pngData) == 0 {
t.Error("ToPNG() should return data even for empty image")
@@ -200,70 +203,15 @@ func TestPNGRenderer_ToPNGEmpty(t *testing.T) {
// Should be valid PNG
reader := bytes.NewReader(pngData)
_, err := png.Decode(reader)
decodedImg, err := png.Decode(reader)
if err != nil {
t.Errorf("ToPNG() returned invalid PNG data: %v", err)
}
}
func TestParseColor(t *testing.T) {
tests := []struct {
input string
opacity float64
expected color.RGBA
}{
{"#ff0000", 1.0, color.RGBA{R: 255, G: 0, B: 0, A: 255}},
{"ff0000", 1.0, color.RGBA{R: 255, G: 0, B: 0, A: 255}},
{"#00ff00", 0.5, color.RGBA{R: 0, G: 255, B: 0, A: 128}},
{"#0000ff", 0.0, color.RGBA{R: 0, G: 0, B: 255, A: 0}},
{"#f00", 1.0, color.RGBA{R: 255, G: 0, B: 0, A: 255}},
{"#0f0", 1.0, color.RGBA{R: 0, G: 255, B: 0, A: 255}},
{"#00f", 1.0, color.RGBA{R: 0, G: 0, B: 255, A: 255}},
{"#ff0000ff", 1.0, color.RGBA{R: 255, G: 0, B: 0, A: 255}},
{"#ff000080", 1.0, color.RGBA{R: 255, G: 0, B: 0, A: 128}},
{"invalid", 1.0, color.RGBA{R: 0, G: 0, B: 0, A: 255}},
{"", 1.0, color.RGBA{R: 0, G: 0, B: 0, A: 255}},
}
for _, test := range tests {
result := parseColor(test.input, test.opacity)
if result != test.expected {
t.Errorf("parseColor(%q, %v) = %v, want %v",
test.input, test.opacity, result, test.expected)
}
}
}
func TestPNGRenderer_ConcurrentAccess(t *testing.T) {
renderer := NewPNGRenderer(100)
// Test concurrent access to ensure thread safety
done := make(chan bool, 10)
for i := 0; i < 10; i++ {
go func(id int) {
renderer.BeginShape("#ff0000")
points := []engine.Point{
{X: float64(id * 5), Y: float64(id * 5)},
{X: float64(id*5 + 10), Y: float64(id * 5)},
{X: float64(id*5 + 10), Y: float64(id*5 + 10)},
{X: float64(id * 5), Y: float64(id*5 + 10)},
}
renderer.AddPolygon(points)
renderer.EndShape()
done <- true
}(i)
}
// Wait for all goroutines to complete
for i := 0; i < 10; i++ {
<-done
}
// Should be able to generate PNG without issues
pngData := renderer.ToPNG()
if len(pngData) == 0 {
t.Error("concurrent access test failed - no PNG data generated")
// Check dimensions
bounds := decodedImg.Bounds()
if bounds.Max.X != 10 || bounds.Max.Y != 10 {
t.Errorf("decoded image bounds = %v, want 10x10", bounds)
}
}
@@ -282,7 +230,10 @@ func BenchmarkPNGRenderer_ToPNG(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
pngData := renderer.ToPNG()
pngData, err := renderer.ToPNG()
if err != nil {
b.Fatalf("Failed to generate PNG: %v", err)
}
if len(pngData) == 0 {
b.Fatal("ToPNG returned empty data")
}