init
This commit is contained in:
292
internal/renderer/png.go
Normal file
292
internal/renderer/png.go
Normal file
@@ -0,0 +1,292 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user