xml2pdf Playground Source

Go library

Everything the CLI does is available as a Go package, so you can render PDFs inside your own services and tools.

go get github.com/sandeepkumarmishra354/xml-to-pdf
import "github.com/sandeepkumarmishra354/xml-to-pdf/pkg/xml2pdf"

API

Render

func Render(r io.Reader, w io.Writer, opts Options) error

Reads an XML document from r and writes the PDF to w. The whole document is laid out before anything is written, so if Render returns an error, nothing has been written to w.

RenderFile

func RenderFile(in, out string, opts Options) error

Renders the XML file in to the PDF file out. If opts.BaseDir is empty, it defaults to the directory of in. The output file is only created if rendering succeeds. Errors are prefixed with the input path.

LoadData

func LoadData(path string) (map[string]any, error)

Reads a JSON object from a file for use as Options.Data. Numbers keep their original text (12.50 stays 12.50, not 12.5).

ParseData

func ParseData(r io.Reader) (map[string]any, error)

Like LoadData, but reads from any reader, such as a request body or an inline string (strings.NewReader(s)). The input must be exactly one JSON object.

MergeData

func MergeData(base, overlay map[string]any) map[string]any

Deep-merges overlay into base and returns a new map. Nested objects are merged key by key, and any other value in overlay (including arrays and null) replaces the one in base. Neither argument is modified. This is what the CLI uses to combine several --data and --json sources.

defaults, _ := xml2pdf.LoadData("defaults.json")
order, _ := xml2pdf.ParseData(r.Body)
opts := xml2pdf.Options{Data: xml2pdf.MergeData(defaults, order)}

Options

type Options struct {
	// BaseDir resolves relative image paths. RenderFile defaults it to the
	// directory of the input file; Render defaults it to the working directory.
	BaseDir string
	// Data provides values for ${name} lookups and arrays for <for-each>.
	// Nested values are addressed as ${a.b} and ${list.0.c}.
	Data map[string]any
	// Vars override Data and document constants.
	Vars map[string]string
	// CreationDate is written to the PDF metadata; zero means now.
	CreationDate time.Time
	// FS, when set, is where image files are read from instead of the OS
	// filesystem, e.g. an embed.FS or fstest.MapFS. Image paths in the
	// document are then slash-separated and relative to BaseDir inside FS.
	FS fs.FS
}

Data can hold decoded JSON (map[string]any, []any) as well as ordinary Go maps with string keys and slices, such as []map[string]any or map[string]string. Scalars can be strings, booleans, any integer or float type, json.Number, or anything implementing fmt.Stringer. Struct fields aren't read; convert structs to maps, or marshal them to JSON and use LoadData.

Examples

Render a file

data, err := xml2pdf.LoadData("invoice.json")
if err != nil {
	log.Fatal(err)
}
err = xml2pdf.RenderFile("invoice.xml", "invoice.pdf", xml2pdf.Options{
	Data: data,
	Vars: map[string]string{"status": "PAID"},
})
if err != nil {
	log.Fatal(err) // e.g. "invoice.xml: line 41: undefined variable \"customer.nmae\""
}

Serve PDFs over HTTP

//go:embed templates/invoice.xml
var invoiceTemplate []byte

func invoiceHandler(w http.ResponseWriter, r *http.Request) {
	inv, err := loadInvoice(r.URL.Query().Get("id")) // your code
	if err != nil {
		http.Error(w, err.Error(), http.StatusNotFound)
		return
	}
	var buf bytes.Buffer
	err = xml2pdf.Render(bytes.NewReader(invoiceTemplate), &buf, xml2pdf.Options{
		BaseDir: "templates",
		Data: map[string]any{
			"invoice_no": inv.Number,
			"customer":   map[string]any{"name": inv.Customer.Name, "address": inv.Customer.Address},
			"items":      inv.LineItems, // e.g. []map[string]any
			"total":      inv.Total,
		},
	})
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/pdf")
	w.Write(buf.Bytes())
}

Rendering into a buffer first means you can still return a proper error status if the template fails.

Reproducible output

PDFs record their creation time. Fix it to get byte-identical output for the same input, which is useful for tests and caching:

opts := xml2pdf.Options{CreationDate: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)}

Templates and images from embed.FS

Ship templates and their images inside your binary. Set Options.FS, and image paths in the document are read from it:

//go:embed templates
var templates embed.FS

func renderInvoice(w io.Writer, data map[string]any) error {
	src, err := templates.Open("templates/invoice.xml")
	if err != nil {
		return err
	}
	defer src.Close()
	return xml2pdf.Render(src, w, xml2pdf.Options{
		FS:      templates,
		BaseDir: "templates", // <img src="logo.png"/> reads templates/logo.png
		Data:    data,
	})
}

Concurrency

Each call to Render or RenderFile is independent and shares no mutable state, so you can call them from many goroutines at once.

Package layout

Only pkg/xml2pdf is a public API. Everything under internal/ may change without notice.

PackageRole
cmd/xml2pdfThe command-line tool.
pkg/xml2pdfThe public Go API.
internal/parserXML → node tree, keeping mixed content and line numbers.
internal/vars${...} resolution and <for-each> expansion.
internal/modelTyped document model and validation.
internal/styleColors, fonts, named styles and the cascade.
internal/unitsLength parsing.
internal/layoutText wrapping, tables and pagination → drawing operations.
internal/renderDrawing operations → PDF, via go-pdf/fpdf.