package renderer import ( "bytes" "image/color" "image/png" "testing" "github.com/kevin/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.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()) } } func TestPNGRenderer_SetBackground(t *testing.T) { renderer := NewPNGRenderer(50) renderer.SetBackground("#ff0000", 1.0) if !renderer.hasBackground { t.Error("hasBackground should be true") } 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) } } func TestPNGRenderer_SetBackgroundWithOpacity(t *testing.T) { renderer := NewPNGRenderer(50) 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) } } 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) } renderer.EndShape() // EndShape is a no-op for PNG, just verify it doesn't panic } func TestPNGRenderer_AddPolygon(t *testing.T) { renderer := NewPNGRenderer(100) renderer.BeginShape("#ff0000") // Create a simple triangle points := []engine.Point{ {X: 10, Y: 10}, {X: 30, Y: 10}, {X: 20, Y: 30}, } 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) { renderer := NewPNGRenderer(100) renderer.BeginShape("#ff0000") // Empty polygon should not panic renderer.AddPolygon([]engine.Point{}) // Polygon with < 3 points should not panic renderer.AddPolygon([]engine.Point{{X: 10, Y: 10}}) renderer.AddPolygon([]engine.Point{{X: 10, Y: 10}, {X: 20, Y: 20}}) } func TestPNGRenderer_AddCircle(t *testing.T) { renderer := NewPNGRenderer(100) renderer.BeginShape("#00ff00") // Circle with center at (50, 50) and radius 20 means topLeft at (30, 30) and size 40 topLeft := engine.Point{X: 30, Y: 30} size := 40.0 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) { renderer := NewPNGRenderer(100) // First fill with background 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 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) { 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) pngData := renderer.ToPNG() if len(pngData) == 0 { t.Error("ToPNG() 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("ToPNG() returned invalid PNG data: %v", err) } // Check dimensions bounds := decodedImg.Bounds() if bounds.Max.X != 50 || bounds.Max.Y != 50 { t.Errorf("decoded image bounds = %v, want 50x50", bounds) } } func TestPNGRenderer_ToPNGEmpty(t *testing.T) { renderer := NewPNGRenderer(10) pngData := renderer.ToPNG() if len(pngData) == 0 { t.Error("ToPNG() should return data even for empty image") } // Should be valid PNG reader := bytes.NewReader(pngData) _, 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") } } func BenchmarkPNGRenderer_ToPNG(b *testing.B) { renderer := NewPNGRenderer(200) renderer.SetBackground("#ffffff", 1.0) // Add some shapes for a realistic benchmark renderer.BeginShape("#ff0000") renderer.AddPolygon([]engine.Point{ {X: 20, Y: 20}, {X: 80, Y: 20}, {X: 80, Y: 80}, {X: 20, Y: 80}, }) renderer.BeginShape("#00ff00") renderer.AddCircle(engine.Point{X: 100, Y: 100}, 30, false) b.ResetTimer() for i := 0; i < b.N; i++ { pngData := renderer.ToPNG() if len(pngData) == 0 { b.Fatal("ToPNG returned empty data") } } }