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.
| Package | Role |
|---|---|
cmd/xml2pdf | The command-line tool. |
pkg/xml2pdf | The public Go API. |
internal/parser | XML → node tree, keeping mixed content and line numbers. |
internal/vars | ${...} resolution and <for-each> expansion. |
internal/model | Typed document model and validation. |
internal/style | Colors, fonts, named styles and the cascade. |
internal/units | Length parsing. |
internal/layout | Text wrapping, tables and pagination → drawing operations. |
internal/render | Drawing operations → PDF, via go-pdf/fpdf. |