使用Golang Draw2d生成SVG文件体积异常偏大问题
draw2d生成NEXRAD雷达图SVG体积过大问题排查与修复
问题现象
搭配draw2d库与go-nexrad库开发雷达图渲染功能时,同配置下生成的PNG文件大小不足10MB,对应SVG文件体积可达28MB。核心渲染逻辑代码如下:
func render(out string, radials []*archive2.Message31, label string) { width := float64(imageSize) height := float64(imageSize) PNGcanvas := image.NewRGBA(image.Rect(0, 0, int(width), int(height))) draw.Draw(PNGcanvas, PNGcanvas.Bounds(), image.Black, image.ZP, draw.Src) PNGgc := draw2dimg.NewGraphicContext(PNGcanvas) SVGcanvas := draw2dsvg.NewSvg() SVGcanvas.Width = strconv.Itoa(int(width)) + "px" SVGcanvas.Height = strconv.Itoa(int(width)) + "px" SVGgc := draw2dsvg.NewGraphicContext(SVGcanvas) xc := width / 2 yc := height / 2 pxPerKm := width / 2 / 460 firstGatePx := float64(radials[0].ReflectivityData.DataMomentRange) / 1000 * pxPerKm gateIntervalKm := float64(radials[0].ReflectivityData.DataMomentRangeSampleInterval) / 1000 gateWidthPx := gateIntervalKm * pxPerKm t := time.Now() log.Println("rendering radials") // valueDist := map[float32]int{} for _, radial := range radials { // round to the nearest rounded azimuth for the given resolution. // ex: for radial 20.5432, round to 20.5 azimuthAngle := float64(radial.Header.AzimuthAngle) - 90 if azimuthAngle < 0 { azimuthAngle = 360.0 + azimuthAngle } azimuthSpacing := radial.Header.AzimuthResolutionSpacing() azimuth := math.Floor(azimuthAngle) if math.Floor(azimuthAngle+azimuthSpacing) > azimuth { azimuth += azimuthSpacing } startAngle := azimuth * (math.Pi / 180.0) /* angles are specified */ endAngle := azimuthSpacing * (math.Pi / 180.0) /* clockwise in radians */ // start drawing gates from the start of the first gate distanceX, distanceY := firstGatePx, firstGatePx if vectorize == "png" { PNGgc.SetLineWidth(gateWidthPx + 1) } else if vectorize == "svg" { SVGgc.SetLineWidth(gateWidthPx + 1) } if vectorize == "png" { PNGgc.SetLineCap(draw2d.ButtCap) } else if vectorize == "svg" { SVGgc.SetLineCap(draw2d.ButtCap) } var gates []float32 switch product { case "vel": gates = radial.VelocityData.ScaledData() case "sw": gates = radial.SwData.ScaledData() case "rho": gates = radial.RhoData.ScaledData() default: gates = radial.ReflectivityData.ScaledData() } numGates := len(gates) for i, v := range gates { if v != archive2.MomentDataBelowThreshold { //fmt.Println(gateWidthPx) if i == 0 { SVGgc.SetLineWidth(0) } else if i > 0 { SVGgc.SetLineWidth(gateWidthPx + 1) } // valueDist[v] += 1 if vectorize == "png" { PNGgc.MoveTo(xc+math.Cos(startAngle)*distanceX, yc+math.Sin(startAngle)*distanceY) } else if vectorize == "svg" { SVGgc.MoveTo(xc+math.Cos(startAngle)*distanceX, yc+math.Sin(startAngle)*distanceY) } // make the gates connect visually by extending arcs so there is no space between adjacent gates. if i == 0 { if vectorize == "png" { PNGgc.ArcTo(xc, yc, distanceX, distanceY, startAngle-.001, endAngle+.001) } else if vectorize == "svg" { SVGgc.ArcTo(xc, yc, distanceX, distanceY, startAngle-.001, endAngle+.001) } } else if i == numGates-1 { if vectorize == "png" { PNGgc.ArcTo(xc, yc, distanceX, distanceY, startAngle, endAngle) } else if vectorize == "svg" { SVGgc.ArcTo(xc, yc, distanceX, distanceY, startAngle, endAngle) } } else { if vectorize == "png" { PNGgc.ArcTo(xc, yc, distanceX, distanceY, startAngle, endAngle+.001) } else if vectorize == "svg" { SVGgc.ArcTo(xc, yc, distanceX, distanceY, startAngle, endAngle+.001) } } if vectorize == "png" { PNGgc.SetStrokeColor(colorSchemes[product][colorScheme](v)) } else if vectorize == "svg" { SVGgc.SetStrokeColor(colorSchemes[product][colorScheme](v)) } if vectorize == "png" { PNGgc.Stroke() } else if vectorize == "svg" { SVGgc.Stroke() } } distanceX += gateWidthPx distanceY += gateWidthPx azimuth += radial.Header.AzimuthResolutionSpacing() } } // fmt.Println(valueDist) if renderLabel { if vectorize == "png" { addLabel(PNGcanvas, int(width-495.0), int(height-10.0), label) } else if vectorize == "svg" { logrus.Warn("Labels cannot be drawn on an SVG image, ignoring -L flag") } } // Save to file if vectorize == "png" { draw2dimg.SaveToPngFile(out, PNGcanvas) fmt.Println("Finished in", time.Since(t)) } else if vectorize == "svg" { draw2dsvg.SaveToSvgFile(out, SVGcanvas) fmt.Println("Finished in", time.Since(t)) } }
初步排查推测是SVG生成逻辑效率低下,疑似采用逐像素渲染而非仅记录路径坐标,尝试调用SVGgc.setDPI()调整DPI参数未取得效果,问题定位在draw2d库调用段。
根本原因
- 核心问题是在每个雷达距离门(gate)的循环内部单独调用
Stroke():draw2d的SVG后端每次执行Stroke()都会生成独立的<path>标签,每个标签都携带完整的坐标、样式属性。单个体扫包含数十万级别的距离门,就会生成等量的独立路径,大量重复属性、碎片化路径直接撑大文件体积。 - 冗余样式设置:线宽、线帽这类固定参数被放在循环内反复调用,相同的样式属性会重复写入每个独立路径,进一步放大冗余。
- draw2d的SVG后端默认不做路径合并、坐标精度压缩,会完整输出所有浮点坐标,也会贡献一部分体积,但不是核心诱因。
修复方案
- 批量合并同样式路径,减少
Stroke()调用次数:相同色值、相同线宽的连续弧段不要每画一段就执行一次描边,连续追加路径点,攒完一批同样式的弧段后再统一调用一次Stroke(),可以把路径标签数量降低2-3个数量级。比如同方位角下连续多个同色值的gate,可以直接连起来绘制,不需要每个gate单独起路径。 - 固定样式初始化外置:线宽、线帽这类全局固定的参数,只需要在初始化SVG图形上下文时设置一次即可,不需要在每层循环内重复设置。
- 压缩坐标精度:draw2d默认输出的浮点坐标保留多位小数,雷达图场景下坐标保留1-2位小数视觉上无差异,可以在保存SVG前对坐标值做四舍五入处理,也可以用SVG压缩工具做后处理,通常能再把体积压缩到原来的1/2到1/3。
- 移除无效绘制逻辑:
i==0时设置SetLineWidth(0)的逻辑对SVG无效,线宽为0依然会生成路径节点,可以直接跳过第一个gate的路径绘制,减少无效节点。
内容的提问来源于stack exchange,提问作者SteepAtticStairs
相关产品推荐
相关产品推荐

