package svg import ( "fmt" "math" "sort" "strings" ) // Info summarizes a parsed document: element counts, colors, the // drawing's bounding box and warnings (things drawn outside the // canvas), so an agent can verify a build without looking at pixels. func Info(d *Doc) string { counts := map[string]int{} colors := map[string]bool{} minX, minY := math.Inf(1), math.Inf(1) maxX, maxY := math.Inf(-1), math.Inf(-1) var warnings []string var walk func(els []*Elem) walk = func(els []*Elem) { for _, el := range els { if el.Kind == "group" { counts["group"]++ walk(el.Kids) continue } counts[el.Kind]++ for _, key := range []string{"fill", "stroke"} { if c := el.Attrs[key]; c != "" && c != "none" && c != "transparent" { colors[c] = true } } x0, y0, x1, y1, ok := bbox(el) if !ok { continue } minX = math.Min(minX, x0) minY = math.Min(minY, y0) maxX = math.Max(maxX, x1) maxY = math.Max(maxY, y1) if x1 < 0 || y1 < 0 || x0 > d.W || y0 > d.H { warnings = append(warnings, fmt.Sprintf("line %d: %s is entirely outside the canvas", el.Line, el.Kind)) } else if x0 < 0 || y0 < 0 || x1 > d.W || y1 > d.H { warnings = append(warnings, fmt.Sprintf("line %d: %s sticks outside the canvas", el.Line, el.Kind)) } } } walk(d.Elems) var b strings.Builder fmt.Fprintf(&b, "canvas: %gx%g", d.W, d.H) if d.Bg != "" { fmt.Fprintf(&b, " bg: %s", d.Bg) } b.WriteString("\n") kinds := make([]string, 0, len(counts)) for k := range counts { kinds = append(kinds, k) } sort.Strings(kinds) total := 0 parts := make([]string, 0, len(kinds)) for _, k := range kinds { parts = append(parts, fmt.Sprintf("%s:%d", k, counts[k])) if k != "group" { total += counts[k] } } fmt.Fprintf(&b, "elements: %d (%s)\n", total, strings.Join(parts, " ")) if len(colors) > 0 { cl := make([]string, 0, len(colors)) for c := range colors { cl = append(cl, c) } sort.Strings(cl) fmt.Fprintf(&b, "colors: %s\n", strings.Join(cl, " ")) } if total > 0 && !math.IsInf(minX, 1) { fmt.Fprintf(&b, "drawing bbox: %.4g,%.4g .. %.4g,%.4g\n", minX, minY, maxX, maxY) } for _, w := range warnings { fmt.Fprintf(&b, "warning: %s\n", w) } return b.String() } // bbox computes an element's geometric bounding box (stroke width and // transforms not included — good enough for out-of-canvas warnings). func bbox(el *Elem) (x0, y0, x1, y1 float64, ok bool) { switch el.Kind { case "rect": return el.Nums[0], el.Nums[1], el.Nums[0] + el.Nums[2], el.Nums[1] + el.Nums[3], true case "circle": cx, cy, r := el.Nums[0], el.Nums[1], el.Nums[2] return cx - r, cy - r, cx + r, cy + r, true case "ellipse": cx, cy, rx, ry := el.Nums[0], el.Nums[1], el.Nums[2], el.Nums[3] return cx - rx, cy - ry, cx + rx, cy + ry, true case "line": return math.Min(el.Nums[0], el.Nums[2]), math.Min(el.Nums[1], el.Nums[3]), math.Max(el.Nums[0], el.Nums[2]), math.Max(el.Nums[1], el.Nums[3]), true case "polyline", "polygon": return pointsBBox(el.Points) case "path": subs, err := flattenPath(el.D) if err != nil { return 0, 0, 0, 0, false } var all [][2]float64 for _, sp := range subs { all = append(all, sp...) } return pointsBBox(all) case "text": size := attrFloat(el.Attrs, "font-size", 16) w := 0.6 * size * float64(len([]rune(el.Text))) x, y := el.Nums[0], el.Nums[1] switch el.Attrs["text-anchor"] { case "middle": x -= w / 2 case "end": x -= w } return x, y - size, x + w, y, true } return 0, 0, 0, 0, false } func pointsBBox(pts [][2]float64) (x0, y0, x1, y1 float64, ok bool) { if len(pts) == 0 { return 0, 0, 0, 0, false } x0, y0 = pts[0][0], pts[0][1] x1, y1 = x0, y0 for _, p := range pts { x0 = math.Min(x0, p[0]) y0 = math.Min(y0, p[1]) x1 = math.Max(x1, p[0]) y1 = math.Max(y1, p[1]) } return x0, y0, x1, y1, true }