package renderer import ( "bytes" "image" "image/color" "image/draw" "image/png" "math" "strconv" "strings" "sync" "github.com/kevin/go-jdenticon/internal/engine" ) // PNGRenderer implements the Renderer interface for PNG output type PNGRenderer struct { *BaseRenderer img *image.RGBA currentColor color.RGBA background color.RGBA hasBackground bool mu sync.RWMutex // For thread safety in concurrent generation } // bufferPool provides buffer pooling for efficient PNG generation var bufferPool = sync.Pool{ New: func() interface{} { return &bytes.Buffer{} }, } // NewPNGRenderer creates a new PNG renderer with the specified icon size func NewPNGRenderer(iconSize int) *PNGRenderer { return &PNGRenderer{ BaseRenderer: NewBaseRenderer(iconSize), img: image.NewRGBA(image.Rect(0, 0, iconSize, iconSize)), } } // SetBackground sets the background color and opacity func (r *PNGRenderer) SetBackground(fillColor string, opacity float64) { r.mu.Lock() defer r.mu.Unlock() r.BaseRenderer.SetBackground(fillColor, opacity) r.background = parseColor(fillColor, opacity) r.hasBackground = opacity > 0 if r.hasBackground { // Fill the entire image with background color draw.Draw(r.img, r.img.Bounds(), &image.Uniform{r.background}, image.Point{}, draw.Src) } } // BeginShape marks the beginning of a new shape with the specified color func (r *PNGRenderer) BeginShape(fillColor string) { r.mu.Lock() defer r.mu.Unlock() r.BaseRenderer.BeginShape(fillColor) r.currentColor = parseColor(fillColor, 1.0) } // EndShape marks the end of the currently drawn shape func (r *PNGRenderer) EndShape() { // No action needed for PNG - shapes are drawn immediately } // AddPolygon adds a polygon with the current fill color to the image func (r *PNGRenderer) AddPolygon(points []engine.Point) { if len(points) == 0 { return } r.mu.Lock() defer r.mu.Unlock() // Convert engine.Point to image coordinates imagePoints := make([]image.Point, len(points)) for i, p := range points { imagePoints[i] = image.Point{ X: int(math.Round(p.X)), Y: int(math.Round(p.Y)), } } // Fill polygon using scanline algorithm r.fillPolygon(imagePoints) } // AddCircle adds a circle with the current fill color to the image func (r *PNGRenderer) AddCircle(topLeft engine.Point, size float64, invert bool) { r.mu.Lock() defer r.mu.Unlock() radius := size / 2 centerX := int(math.Round(topLeft.X + radius)) centerY := int(math.Round(topLeft.Y + radius)) radiusInt := int(math.Round(radius)) // Use Bresenham's circle algorithm for anti-aliased circle drawing r.drawCircle(centerX, centerY, radiusInt, invert) } // ToPNG generates the final PNG image data func (r *PNGRenderer) ToPNG() []byte { r.mu.RLock() defer r.mu.RUnlock() buf := bufferPool.Get().(*bytes.Buffer) buf.Reset() defer bufferPool.Put(buf) // Encode to PNG with compression encoder := &png.Encoder{ CompressionLevel: png.BestCompression, } if err := encoder.Encode(buf, r.img); err != nil { return nil } // Return a copy of the buffer data result := make([]byte, buf.Len()) copy(result, buf.Bytes()) return result } // parseColor converts a hex color string to RGBA color func parseColor(hexColor string, opacity float64) color.RGBA { // Remove # prefix if present hexColor = strings.TrimPrefix(hexColor, "#") // Default to black if parsing fails var r, g, b uint8 = 0, 0, 0 switch len(hexColor) { case 3: // Short form: #RGB -> #RRGGBB if val, err := strconv.ParseUint(hexColor, 16, 12); err == nil { r = uint8((val >> 8 & 0xF) * 17) g = uint8((val >> 4 & 0xF) * 17) b = uint8((val & 0xF) * 17) } case 6: // Full form: #RRGGBB if val, err := strconv.ParseUint(hexColor, 16, 24); err == nil { r = uint8(val >> 16) g = uint8(val >> 8) b = uint8(val) } case 8: // With alpha: #RRGGBBAA if val, err := strconv.ParseUint(hexColor, 16, 32); err == nil { r = uint8(val >> 24) g = uint8(val >> 16) b = uint8(val >> 8) // Override opacity with alpha from color opacity = float64(uint8(val)) / 255.0 } } alpha := uint8(math.Round(opacity * 255)) return color.RGBA{R: r, G: g, B: b, A: alpha} } // fillPolygon fills a polygon using a scanline algorithm func (r *PNGRenderer) fillPolygon(points []image.Point) { if len(points) < 3 { return } // Find bounding box minY, maxY := points[0].Y, points[0].Y for _, p := range points[1:] { if p.Y < minY { minY = p.Y } if p.Y > maxY { maxY = p.Y } } // Ensure bounds are within image bounds := r.img.Bounds() if minY < bounds.Min.Y { minY = bounds.Min.Y } if maxY >= bounds.Max.Y { maxY = bounds.Max.Y - 1 } // For each scanline, find intersections and fill for y := minY; y <= maxY; y++ { intersections := r.getIntersections(points, y) if len(intersections) < 2 { continue } // Sort intersections and fill between pairs for i := 0; i < len(intersections); i += 2 { if i+1 < len(intersections) { x1, x2 := intersections[i], intersections[i+1] if x1 > x2 { x1, x2 = x2, x1 } // Clamp to image bounds if x1 < bounds.Min.X { x1 = bounds.Min.X } if x2 >= bounds.Max.X { x2 = bounds.Max.X - 1 } // Fill the horizontal line for x := x1; x <= x2; x++ { r.img.SetRGBA(x, y, r.currentColor) } } } } } // getIntersections finds x-coordinates where a horizontal line intersects polygon edges func (r *PNGRenderer) getIntersections(points []image.Point, y int) []int { var intersections []int n := len(points) for i := 0; i < n; i++ { j := (i + 1) % n p1, p2 := points[i], points[j] // Check if the edge crosses the scanline if (p1.Y <= y && p2.Y > y) || (p2.Y <= y && p1.Y > y) { // Calculate intersection x-coordinate x := p1.X + (y-p1.Y)*(p2.X-p1.X)/(p2.Y-p1.Y) intersections = append(intersections, x) } } // Sort intersections for i := 0; i < len(intersections)-1; i++ { for j := i + 1; j < len(intersections); j++ { if intersections[i] > intersections[j] { intersections[i], intersections[j] = intersections[j], intersections[i] } } } return intersections } // drawCircle draws a filled circle using Bresenham's algorithm func (r *PNGRenderer) drawCircle(centerX, centerY, radius int, invert bool) { bounds := r.img.Bounds() // For filled circle, we'll draw it by filling horizontal lines for y := -radius; y <= radius; y++ { actualY := centerY + y if actualY < bounds.Min.Y || actualY >= bounds.Max.Y { continue } // Calculate x extent for this y x := int(math.Sqrt(float64(radius*radius - y*y))) x1, x2 := centerX-x, centerX+x // Clamp to image bounds if x1 < bounds.Min.X { x1 = bounds.Min.X } if x2 >= bounds.Max.X { x2 = bounds.Max.X - 1 } // Fill the horizontal line for x := x1; x <= x2; x++ { if invert { // For inverted circles, we need to punch a hole // This would typically be handled by a compositing mode // For now, we'll set to transparent r.img.SetRGBA(x, actualY, color.RGBA{0, 0, 0, 0}) } else { r.img.SetRGBA(x, actualY, r.currentColor) } } } }