package render import ( "encoding/binary" "fmt" "math" "strings" ) const canonicalToPDF = 72.0 / 300.0 // 0.24 // maxPointDataSize is the maximum allowed size of point data (4 MB). const maxPointDataSize = 4 * 1024 * 1024 // PageSizePt returns the page dimensions in PDF points (72 DPI). func PageSizePt(pageSize string) (float64, float64) { switch pageSize { case "LARGE": return 3300 * canonicalToPDF, 5100 * canonicalToPDF // 792 x 1224 default: // REGULAR return 2550 * canonicalToPDF, 3300 * canonicalToPDF // 612 x 792 } } type Stroke struct { PenSize float32 Color int32 Style string PointData []byte StrokeOrder int } // RenderSVG renders a page's strokes as an SVG document. func RenderSVG(pageSize string, strokes []Stroke) (string, error) { w, h := PageSizePt(pageSize) var b strings.Builder fmt.Fprintf(&b, ``, w, h, w, h) b.WriteString("\n") // White background fmt.Fprintf(&b, ``, w, h) b.WriteString("\n") for _, s := range strokes { points, err := decodePoints(s.PointData) if err != nil { continue } if len(points) < 4 { continue } penW := float64(s.PenSize) * canonicalToPDF color := colorToCSS(s.Color) // Build path data var path strings.Builder fmt.Fprintf(&path, "M %.2f %.2f", points[0]*canonicalToPDF, points[1]*canonicalToPDF) for i := 2; i < len(points)-1; i += 2 { fmt.Fprintf(&path, " L %.2f %.2f", points[i]*canonicalToPDF, points[i+1]*canonicalToPDF) } attrs := fmt.Sprintf(`stroke="%s" stroke-width="%.2f" stroke-linecap="round" stroke-linejoin="round" fill="none"`, color, penW) if s.Style == "dashed" { attrs += fmt.Sprintf(` stroke-dasharray="%.1f %.1f"`, 7.2, 4.8) } fmt.Fprintf(&b, ``, path.String(), attrs) b.WriteString("\n") // Arrow heads if (s.Style == "arrow" || s.Style == "double_arrow") && len(points) >= 4 { x1, y1 := points[0]*canonicalToPDF, points[1]*canonicalToPDF x2, y2 := points[2]*canonicalToPDF, points[3]*canonicalToPDF b.WriteString(renderArrowHeads(x1, y1, x2, y2, s.Style, penW, color)) } } b.WriteString("") return b.String(), nil } func renderArrowHeads(x1, y1, x2, y2 float64, style string, penW float64, color string) string { arrowLen := 40.0 * canonicalToPDF arrowAngle := 25.0 * math.Pi / 180.0 dx := x2 - x1 dy := y2 - y1 angle := math.Atan2(dy, dx) var b strings.Builder attrs := fmt.Sprintf(`stroke="%s" stroke-width="%.2f" stroke-linecap="round"`, color, penW) // End arrow ax1 := x2 - arrowLen*math.Cos(angle-arrowAngle) ay1 := y2 - arrowLen*math.Sin(angle-arrowAngle) ax2 := x2 - arrowLen*math.Cos(angle+arrowAngle) ay2 := y2 - arrowLen*math.Sin(angle+arrowAngle) fmt.Fprintf(&b, ``, x2, y2, ax1, ay1, attrs) b.WriteString("\n") fmt.Fprintf(&b, ``, x2, y2, ax2, ay2, attrs) b.WriteString("\n") if style == "double_arrow" { revAngle := angle + math.Pi bx1 := x1 - arrowLen*math.Cos(revAngle-arrowAngle) by1 := y1 - arrowLen*math.Sin(revAngle-arrowAngle) bx2 := x1 - arrowLen*math.Cos(revAngle+arrowAngle) by2 := y1 - arrowLen*math.Sin(revAngle+arrowAngle) fmt.Fprintf(&b, ``, x1, y1, bx1, by1, attrs) b.WriteString("\n") fmt.Fprintf(&b, ``, x1, y1, bx2, by2, attrs) b.WriteString("\n") } return b.String() } // decodePoints decodes a byte slice of little-endian float32 values into float64. // Returns an error if the data length is not a multiple of 4, exceeds maxPointDataSize, // or contains NaN/Inf values. func decodePoints(data []byte) ([]float64, error) { if len(data)%4 != 0 { return nil, fmt.Errorf("point data length %d is not a multiple of 4", len(data)) } if len(data) > maxPointDataSize { return nil, fmt.Errorf("point data size %d exceeds maximum %d", len(data), maxPointDataSize) } count := len(data) / 4 points := make([]float64, 0, count) for i := 0; i < count; i++ { bits := binary.LittleEndian.Uint32(data[i*4 : (i+1)*4]) v := float64(math.Float32frombits(bits)) if math.IsNaN(v) || math.IsInf(v, 0) { return nil, fmt.Errorf("point data contains NaN or Inf at index %d", i) } points = append(points, v) } return points, nil } func colorToCSS(argb int32) string { r := (argb >> 16) & 0xFF g := (argb >> 8) & 0xFF b := argb & 0xFF if r == 0 && g == 0 && b == 0 { return "black" } return fmt.Sprintf("rgb(%d,%d,%d)", r, g, b) }