SVG is XML, so parsing it is easy in any language. Rendering it to PNG accurately is the hard part — SVG has filters, gradients, complex paths, and font rules that range from straightforward to genuinely hard. Pure-Go libraries handle the common 80%; librsvg handles the rest.
Method 1: oksvg + rasterx (pure Go)
oksvg parses SVG; rasterx renders it. Both are pure Go — no native deps, runs anywhere Go runs.
go get github.com/srwiley/oksvg
go get github.com/srwiley/rasterx
package main
import (
"fmt"
"image"
"image/png"
"os"
"github.com/srwiley/oksvg"
"github.com/srwiley/rasterx"
)
func svgToPNG(inPath, outPath string, width, height int) error {
in, err := os.Open(inPath)
if err != nil {
return fmt.Errorf("open: %w", err)
}
defer in.Close()
icon, err := oksvg.ReadIconStream(in)
if err != nil {
return fmt.Errorf("parse svg: %w", err)
}
icon.SetTarget(0, 0, float64(width), float64(height))
rgba := image.NewRGBA(image.Rect(0, 0, width, height))
scanner := rasterx.NewScannerGV(width, height, rgba, rgba.Bounds())
raster := rasterx.NewDasher(width, height, scanner)
icon.Draw(raster, 1.0)
out, err := os.Create(outPath)
if err != nil {
return err
}
defer out.Close()
return png.Encode(out, rgba)
}
func main() {
if err := svgToPNG("logo.svg", "logo.png", 512, 512); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Three things to know:
- SetTarget controls output size. The SVG's viewBox is mapped to (0, 0, width, height). For 2x DPI rendering, double both.
- Filters and gradients have limited support. Linear and radial gradients work; complex filter chains (feGaussianBlur, feColorMatrix) may not render.
- No font rendering. SVG
<text>elements are not rendered. For text, convert to paths in your SVG editor first.
Method 2: librsvg via os/exec (full SVG support)
librsvg is GNOME's SVG renderer — used by GIMP, Inkscape, and the GNOME desktop. It handles every SVG feature including text, filters, and embedded fonts.
apt install librsvg2-bin # provides rsvg-convert
# macOS: brew install librsvg
package main
import (
"context"
"fmt"
"os"
"os/exec"
"strconv"
"time"
)
func svgToPNG(ctx context.Context, inPath, outPath string, width, height int) error {
cmd := exec.CommandContext(ctx,
"rsvg-convert",
"-w", strconv.Itoa(width),
"-h", strconv.Itoa(height),
"-f", "png",
"-o", outPath,
inPath,
)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("rsvg-convert: %w (%s)", err, out)
}
return nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := svgToPNG(ctx, "complex-chart.svg", "chart.png", 1024, 768); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Useful flags:
- -w / -h — output width/height in pixels. Use one to preserve aspect.
- -z (zoom) — scale factor (e.g., -z 2 for 2x DPI).
- -d / -p (DPI) — explicit DPI for x/y axes (default 90).
- -b color — background color (e.g., -b white). SVGs are transparent by default.
Method 3: ChangeThisFile API (no install)
If you don't want librsvg in your environment (Lambda, distroless), the API runs it server-side. Free tier covers 1,000 conversions/month.
package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
const apiKey = "ctf_sk_your_key_here"
func svgToPNG(inPath, outPath string) error {
body := &bytes.Buffer{}
w := multipart.NewWriter(body)
f, err := os.Open(inPath)
if err != nil {
return err
}
defer f.Close()
fw, err := w.CreateFormFile("file", "input.svg")
if err != nil {
return err
}
if _, err := io.Copy(fw, f); err != nil {
return err
}
_ = w.WriteField("source", "svg")
_ = w.WriteField("target", "png")
_ = w.Close()
req, err := http.NewRequest("POST", "https://changethisfile.com/v1/convert", body)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", w.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(resp.Body)
return fmt.Errorf("api %d: %s", resp.StatusCode, msg)
}
out, err := os.Create(outPath)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
return err
}
func main() {
if err := svgToPNG("logo.svg", "logo.png"); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
The API accepts width/height as form fields if you need a specific output size — otherwise it uses the SVG's viewBox dimensions.
When to use each
| Approach | Best for | Tradeoff |
|---|---|---|
| oksvg + rasterx | Single-binary, simple SVG (icons, charts), no native deps | Limited filter/text support |
| rsvg-convert via os/exec | Complex SVG with filters, text, embedded fonts | ~5MB install (librsvg2-bin) |
| ChangeThisFile API | Lambda, distroless, multi-tenant SaaS | Network call, file size limit (25MB free) |
CLI alternative: ImageMagick or Inkscape
For one-off conversions or when you're already using these tools elsewhere:
# ImageMagick (uses librsvg internally if available)
convert -density 192 logo.svg -resize 512x512 logo.png
# Inkscape (highest fidelity for designer-authored SVG)
inkscape logo.svg --export-type=png --export-filename=logo.png \
--export-width=512 --export-height=512
Inkscape's renderer is even more accurate than librsvg for designer-authored SVG (it uses the same engine internally). But it's a much larger install (~150MB). For server-side rendering, librsvg is the right balance.
Production tips
- For high-DPI displays, render at 2x. A 256x256 icon for a Retina display should be rendered at 512x512 and downscaled with CSS. Or render at the target pixel density directly.
- Convert text to paths in your SVG source. If oksvg can't render <text> elements, the fix is in the SVG itself — use Illustrator/Inkscape's 'Convert to Outlines' / 'Object to Path' to bake text into vector paths.
- Set a background color for non-transparent output. SVGs are transparent by default. For email or social media use cases that don't support transparency, add a white background:
-b whitein rsvg-convert, or composite onto a white image in oksvg. - Sanitize untrusted SVGs. SVG can contain <script> tags, external image references, and XML entity expansions. For user-uploaded SVGs, run them through a sanitizer (e.g., DOMPurify on Node, bluemonday in Go) before rendering.
For icons, charts, and diagrams in a Go binary, oksvg + rasterx is the right answer. For complex SVG, rsvg-convert. For environments where you can't add native deps, the API. Free tier covers 1,000 conversions/month.