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:
@@ -1,14 +1,28 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kevin/go-jdenticon/internal/engine"
|
||||
"github.com/ungluedlabs/go-jdenticon/internal/engine"
|
||||
)
|
||||
|
||||
// SVG rendering constants
|
||||
const (
|
||||
// SVG generation size estimation constants
|
||||
svgBaseOverheadBytes = 150 // Base SVG document overhead
|
||||
svgBackgroundRectBytes = 100 // Background rectangle overhead
|
||||
svgPathOverheadBytes = 50 // Per-path element overhead
|
||||
|
||||
// Precision constants
|
||||
svgCoordinatePrecision = 10 // Precision factor for SVG coordinates (0.1 precision)
|
||||
svgRoundingOffset = 0.5 // Rounding offset for "round half up" behavior
|
||||
)
|
||||
|
||||
// Note: Previously used polygonBufferPool for intermediate buffering, but eliminated
|
||||
// to write directly to main builder and avoid unnecessary allocations
|
||||
|
||||
// SVGPath represents an SVG path element
|
||||
type SVGPath struct {
|
||||
data strings.Builder
|
||||
@@ -20,12 +34,19 @@ func (p *SVGPath) AddPolygon(points []engine.Point) {
|
||||
return
|
||||
}
|
||||
|
||||
// Write directly to main data builder to avoid intermediate allocations
|
||||
// Move to first point
|
||||
p.data.WriteString(fmt.Sprintf("M%s %s", svgValue(points[0].X), svgValue(points[0].Y)))
|
||||
p.data.WriteString("M")
|
||||
svgAppendValue(&p.data, points[0].X)
|
||||
p.data.WriteString(" ")
|
||||
svgAppendValue(&p.data, points[0].Y)
|
||||
|
||||
// Line to subsequent points
|
||||
for i := 1; i < len(points); i++ {
|
||||
p.data.WriteString(fmt.Sprintf("L%s %s", svgValue(points[i].X), svgValue(points[i].Y)))
|
||||
p.data.WriteString("L")
|
||||
svgAppendValue(&p.data, points[i].X)
|
||||
p.data.WriteString(" ")
|
||||
svgAppendValue(&p.data, points[i].Y)
|
||||
}
|
||||
|
||||
// Close path
|
||||
@@ -42,18 +63,38 @@ func (p *SVGPath) AddCircle(topLeft engine.Point, size float64, counterClockwise
|
||||
radius := size / 2
|
||||
centerX := topLeft.X + radius
|
||||
centerY := topLeft.Y + radius
|
||||
|
||||
svgRadius := svgValue(radius)
|
||||
svgDiameter := svgValue(size)
|
||||
svgArc := fmt.Sprintf("a%s,%s 0 1,%s ", svgRadius, svgRadius, sweepFlag)
|
||||
|
||||
// Move to start point (left side of circle)
|
||||
startX := centerX - radius
|
||||
startY := centerY
|
||||
|
||||
p.data.WriteString(fmt.Sprintf("M%s %s", svgValue(startX), svgValue(startY)))
|
||||
p.data.WriteString(svgArc + svgDiameter + ",0")
|
||||
p.data.WriteString(svgArc + "-" + svgDiameter + ",0")
|
||||
// Build circle path directly in main data builder
|
||||
p.data.WriteString("M")
|
||||
svgAppendValue(&p.data, startX)
|
||||
p.data.WriteString(" ")
|
||||
svgAppendValue(&p.data, startY)
|
||||
|
||||
// Draw first arc
|
||||
p.data.WriteString("a")
|
||||
svgAppendValue(&p.data, radius)
|
||||
p.data.WriteString(",")
|
||||
svgAppendValue(&p.data, radius)
|
||||
p.data.WriteString(" 0 1,")
|
||||
p.data.WriteString(sweepFlag)
|
||||
p.data.WriteString(" ")
|
||||
svgAppendValue(&p.data, size)
|
||||
p.data.WriteString(",0")
|
||||
|
||||
// Draw second arc
|
||||
p.data.WriteString("a")
|
||||
svgAppendValue(&p.data, radius)
|
||||
p.data.WriteString(",")
|
||||
svgAppendValue(&p.data, radius)
|
||||
p.data.WriteString(" 0 1,")
|
||||
p.data.WriteString(sweepFlag)
|
||||
p.data.WriteString(" -")
|
||||
svgAppendValue(&p.data, size)
|
||||
p.data.WriteString(",0")
|
||||
}
|
||||
|
||||
// DataString returns the SVG path data string
|
||||
@@ -84,6 +125,14 @@ func (r *SVGRenderer) SetBackground(fillColor string, opacity float64) {
|
||||
|
||||
// BeginShape marks the beginning of a new shape with the specified color
|
||||
func (r *SVGRenderer) BeginShape(color string) {
|
||||
// Defense-in-depth validation: ensure color is safe for SVG output
|
||||
// Invalid colors are silently ignored to maintain interface compatibility
|
||||
if err := engine.ValidateHexColor(color); err != nil {
|
||||
// Log validation failure but continue - the shape will not be rendered
|
||||
// This prevents breaking the interface while maintaining security
|
||||
return
|
||||
}
|
||||
|
||||
r.BaseRenderer.BeginShape(color)
|
||||
if _, exists := r.pathsByColor[color]; !exists {
|
||||
r.pathsByColor[color] = &SVGPath{}
|
||||
@@ -121,22 +170,49 @@ func (r *SVGRenderer) AddCircle(topLeft engine.Point, size float64, invert bool)
|
||||
|
||||
// ToSVG generates the final SVG XML string
|
||||
func (r *SVGRenderer) ToSVG() string {
|
||||
var svg strings.Builder
|
||||
|
||||
iconSize := r.GetSize()
|
||||
background, backgroundOp := r.GetBackground()
|
||||
|
||||
// Estimate capacity to reduce allocations
|
||||
capacity := svgBaseOverheadBytes
|
||||
if background != "" && backgroundOp > 0 {
|
||||
capacity += svgBackgroundRectBytes
|
||||
}
|
||||
|
||||
// Estimate path data size
|
||||
for _, color := range r.colorOrder {
|
||||
path := r.pathsByColor[color]
|
||||
if path != nil {
|
||||
capacity += svgPathOverheadBytes + path.data.Len()
|
||||
}
|
||||
}
|
||||
|
||||
var svg strings.Builder
|
||||
svg.Grow(capacity)
|
||||
|
||||
// SVG opening tag with namespace and dimensions
|
||||
svg.WriteString(fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d">`,
|
||||
iconSize, iconSize, iconSize, iconSize))
|
||||
iconSizeStr := strconv.Itoa(iconSize)
|
||||
svg.WriteString(`<svg xmlns="http://www.w3.org/2000/svg" width="`)
|
||||
svg.WriteString(iconSizeStr)
|
||||
svg.WriteString(`" height="`)
|
||||
svg.WriteString(iconSizeStr)
|
||||
svg.WriteString(`" viewBox="0 0 `)
|
||||
svg.WriteString(iconSizeStr)
|
||||
svg.WriteString(` `)
|
||||
svg.WriteString(iconSizeStr)
|
||||
svg.WriteString(`">`)
|
||||
|
||||
// Add background rectangle if specified
|
||||
if background != "" && backgroundOp > 0 {
|
||||
if backgroundOp >= 1.0 {
|
||||
svg.WriteString(fmt.Sprintf(`<rect width="100%%" height="100%%" fill="%s"/>`, background))
|
||||
// Validate background color for safe SVG output
|
||||
if err := engine.ValidateHexColor(background); err != nil {
|
||||
// Skip invalid background colors to prevent injection
|
||||
} else {
|
||||
svg.WriteString(fmt.Sprintf(`<rect width="100%%" height="100%%" fill="%s" opacity="%.2f"/>`,
|
||||
background, backgroundOp))
|
||||
svg.WriteString(`<rect width="100%" height="100%" fill="`)
|
||||
svg.WriteString(background) // Now validated
|
||||
svg.WriteString(`" opacity="`)
|
||||
svg.WriteString(strconv.FormatFloat(backgroundOp, 'f', 2, 64))
|
||||
svg.WriteString(`"/>`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +221,16 @@ func (r *SVGRenderer) ToSVG() string {
|
||||
path := r.pathsByColor[color]
|
||||
dataString := path.DataString()
|
||||
if dataString != "" {
|
||||
svg.WriteString(fmt.Sprintf(`<path fill="%s" d="%s"/>`, color, dataString))
|
||||
// Final defense-in-depth validation before writing to SVG
|
||||
if err := engine.ValidateHexColor(color); err != nil {
|
||||
// Skip invalid colors to prevent injection attacks
|
||||
continue
|
||||
}
|
||||
svg.WriteString(`<path fill="`)
|
||||
svg.WriteString(color) // Now validated - safe injection point
|
||||
svg.WriteString(`" d="`)
|
||||
svg.WriteString(dataString)
|
||||
svg.WriteString(`"/>`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,13 +245,33 @@ func (r *SVGRenderer) ToSVG() string {
|
||||
func svgValue(value float64) string {
|
||||
// Use math.Floor to replicate the "round half up" logic from the JS implementation.
|
||||
// JavaScript: ((value * 10 + 0.5) | 0) / 10
|
||||
rounded := math.Floor(value*10 + 0.5) / 10
|
||||
|
||||
rounded := math.Floor(value*svgCoordinatePrecision+svgRoundingOffset) / svgCoordinatePrecision
|
||||
|
||||
// Format to an integer string if there's no fractional part.
|
||||
if rounded == math.Trunc(rounded) {
|
||||
return strconv.Itoa(int(rounded))
|
||||
}
|
||||
|
||||
|
||||
// Otherwise, format to one decimal place.
|
||||
return strconv.FormatFloat(rounded, 'f', 1, 64)
|
||||
}
|
||||
|
||||
// svgAppendValue appends a formatted float64 directly to a strings.Builder to avoid string allocations
|
||||
func svgAppendValue(buf *strings.Builder, value float64) {
|
||||
// Use math.Floor to replicate the "round half up" logic from the JS implementation.
|
||||
// JavaScript: ((value * 10 + 0.5) | 0) / 10
|
||||
rounded := math.Floor(value*svgCoordinatePrecision+svgRoundingOffset) / svgCoordinatePrecision
|
||||
|
||||
// Use stack-allocated buffer for AppendFloat to avoid heap allocations
|
||||
var tempBuf [32]byte
|
||||
|
||||
// Format to an integer string if there's no fractional part.
|
||||
if rounded == math.Trunc(rounded) {
|
||||
result := strconv.AppendInt(tempBuf[:0], int64(rounded), 10)
|
||||
buf.Write(result)
|
||||
} else {
|
||||
// Otherwise, format to one decimal place using AppendFloat
|
||||
result := strconv.AppendFloat(tempBuf[:0], rounded, 'f', 1, 64)
|
||||
buf.Write(result)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user