sr

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 5, 2026 License: MIT Imports: 12 Imported by: 0

README

sr — structural reporting

A banded report formatter. Apply a template to a sequence of records and get a paginated document: bands, groups, page and column headers and footers, running aggregates, and page footers that know the final page count.

A Go library with a CLI over it. Template plus JSON in, PDF out, one static binary.

Status: everything in these documents is implemented and tested: the engine, subreports, the PDF renderer and the command line.

Documentation

doc/template.md Template format: nodes, properties, geometry, ordering rules, font resolution
doc/expressions.md Expression language, predefined names, formatting, variable semantics
doc/layout.md Layout and pagination
doc/printout.md The intermediate document a renderer consumes
doc/render.md PDF rendering: what a renderer decides, and what it must not
doc/cli.md The command line: subcommands, flags, streams, exit codes
doc/decisions.md Why the design is what it is, and what it replaces
example/sakila/ Reference template and dataset
example/invoices/ Second example: both kinds of subreport, region grouping, the remaining variable modes
example/fonts/ Fonts committed so examples resolve identically everywhere

The Go packages are github.com/a1s/sr for the library, github.com/a1s/sr/printout for the document a renderer reads, and github.com/a1s/sr/pdf for the renderer.

How it works

Three artifacts and one language:

  • A template is a KDL v2 document describing page geometry and a tree of bands. Bands are title, summary, header, footer, and detail; groups nest around the detail band.
  • Data is JSON or NDJSON. Field types are declared in the template, which is what turns "19.99" into an exact decimal and "2005-05-24T22:53:30Z" into a time value.
  • A printout is the engine's output: pages of absolutely-positioned marks with nothing evaluable left in them. Renderers do no layout.
  • Expressions are Starlark — sandboxed, with no imports, filesystem, or network.

Layout is speculative: every band is measured before it is committed, then placed, split, or deferred to the next frame. Band splitting, keep-together, orphan and widow control, and correct deferred page counts all follow from that.

Command line

make build      # or: go install github.com/a1s/sr/cmd/sr@latest
sr build --template sakila.kdl --data payments.jsonl --out report.pdf
Flag
-t, --template Template file. Required.
-d, --data JSON array or NDJSON file, - for standard input. Omit for a template with no records.
-o, --out Output path, - for standard output. Extension selects the format: .pdf, .srp.jsonl, .srp.cbor.
--format pdf, jsonl or cbor, when the extension does not say.
--param NAME=VALUE Supply a report parameter. Repeatable. VALUE is text, parsed per the parameter's declared type. A name the template does not declare is an error, not a value that goes nowhere.
--build-time RFC 3339. Fixes BUILD_TIME for reproducible output.
--strict-fonts Resolve only explicitly named font files; fail otherwise.
--allow-overflow Downgrade oversized-band errors to warnings recorded in the printout.
--uncompressed Leave PDF streams uncompressed, to read the file in an editor.
-v, --verbose Report host font diagnostics.

Other subcommands:

sr validate sakila.kdl             # check a template without data
sr inspect report.srp.jsonl        # dump a printout as readable text
sr render report.srp.jsonl -o report.pdf

build writing a printout and render reading one back produce the same PDF as build writing PDF directly, which is what makes printouts worth archiving.

The document goes to standard output and everything about the run to standard error, so -o - pipes. Exit 0 is success, 1 a run that failed, 2 a mistake in the command line; warnings never change it, because they travel in the printout header where archiving keeps them. Full reference: doc/cli.md.

Library

package main

import (
    "time"

    "github.com/a1s/sr"
)

func main() {
    tpl, err := sr.LoadTemplate("sakila.kdl")
    if err != nil {
        panic(err)
    }

    rows := []map[string]any{
        {
            "customer_id": 1,
            "amount":      sr.Decimal("2.99"),
            "rental_date": time.Date(2005, 5, 24, 22, 53, 30, 0, time.UTC),
            "customer":    map[string]any{"first_name": "MARY", "last_name": "SMITH"},
            "film":        map[string]any{"title": "ACADEMY DINOSAUR"},
        },
    }

    out, err := tpl.Build(rows,
        sr.WithParam("period_start", time.Date(2005, 1, 1, 0, 0, 0, 0, time.UTC)),
        sr.WithBuildTime(time.Date(2026, 8, 4, 9, 12, 44, 0, time.UTC)),
        sr.StrictFonts(),
    )
    if err != nil {
        panic(err)
    }

    if err := sr.WritePDF(out, "report.pdf"); err != nil {
        panic(err)
    }
}

sr.WritePDF is pdf.WriteFile, re-exported for the common case of building and rendering in one place. Use the pdf package directly to render to a writer, or to a printout read back from a file:

out, err := printout.ReadFile("report.srp.jsonl")
err = pdf.Write(out, os.Stdout)

A printout also serializes to NDJSON or CBOR with out.WriteFile(path), which is the artifact worth archiving: rendering a printout read back gives the same PDF, byte for byte, as rendering the one it was built from.

Records may be map[string]any or structs; struct fields map to declared members by name, or by an sr:"..." tag. The CLI is a thin shell over this API.

A loaded template also describes itself, which is what a front end needs before it has any data -- the parameters to ask for, the page geometry, the names declared:

info := tpl.Info()                          // parameters, page, groups, fonts, …
check, err := tpl.CheckFonts()              // what each typeface resolved to

CheckFonts is the one check that reads the machine rather than the document, and it is what sr validate reports. A font that does not resolve comes back in check.Failures rather than as an error, so one missing typeface does not hide the next.

Running the examples

sr build -t example/sakila/sakila.kdl -d example/sakila/payments.jsonl -o sakila.pdf

Narrowed to one month, using the template's date parameters:

sr build -t example/sakila/sakila.kdl -d example/sakila/payments.jsonl -o june.pdf --param period_start=2005-06-01 --param period_end=2005-07-01

The second example, which uses both kinds of subreport:

sr build -t example/invoices/invoices.kdl -d example/invoices/invoices.jsonl -o invoices.pdf

sakila.kdl — a payment list grouped by customer in two columns: every band type, a group with its own title and summary, column header and footer, stretch fields, a floating element, six barcode types, an embedded image and a referenced one, both kinds of cross-reference, conditional outline entries, a deferred page count, justified text, and typed parameters.

invoices.kdl — invoices by region with line items: an inline embedded subreport with an arg and its own records, a template= subreport in region_sheet.kdl that paginates itself onto landscape pages of its own, a group using keeptogether with minrows and mintailrows, a summary with swapfooter, an image with embed=#false, a compressed data blob, iter="item" against iter="detail", and the calc modes sakila leaves out.

Between them the two templates use every node in the format and all twelve calc modes. They do not exhaust every property. What they leave out, so that reading them as a reference does not mislead:

Properties layout width / height / landscape, font typeface / data / bold / italic, line backslant, image type, barcode data, data expr, maxheight, format as a date-parse layout on either parameter or member, subreport ownpageno, columns balance
Spellings the x / y aliases for left / top — both examples use left and top throughout
Enumerations barcode types Code93, DataMatrix, QR-M; image scale="cut" and "grow"; dash="dash"
Scopes iter="report" / "page" / "column"; reset="detail" / "item"

Both reference the committed fonts by path, so they build identically on any machine and work under --strict-fonts. Swap file= for typeface= to use whatever the machine has instead.

Reproducible output

The same template over the same data produces byte-identical printouts, and byte-identical PDFs, when --build-time is fixed and --strict-fonts is set -- on any machine, not just across runs on one. Rendering adds no timestamp of its own: a PDF's creation date is the run's BUILD_TIME. Without the first, the run timestamp differs; without the second, output depends on which fonts are installed.

Nothing machine-specific survives in a strict printout. Strict mode admits only fonts the template named by path, and every path the template named — fonts and embed=#false images alike — is written relative to the printout rather than absolute. So a printout and the files it points at move as one tree. The header records which font file each typeface resolved to and by which step, so a difference is diagnosable from the artifact.

License

MIT. See LICENSE.

Documentation

Overview

Package sr applies a banded report template to a sequence of records and produces a paginated document.

A template is a KDL v2 document; records are Go maps or structs, or JSON on the way in; the result is a printout -- pages of absolutely-positioned marks with nothing evaluable left in them.

tpl, err := sr.LoadTemplate("sakila.kdl")
out, err := tpl.Build(rows, sr.StrictFonts())

The specification the implementation follows is in the doc directory: template.md, expressions.md, layout.md and printout.md.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Decimal

func Decimal(text string) decimal.Decimal

Decimal builds an exact decimal value, the type a member declared type="decimal" produces.

func WritePDF

func WritePDF(doc *Printout, path string, options ...pdf.Option) error

WritePDF renders a printout to a PDF file.

It is package pdf's WriteFile, re-exported for the common case where a caller builds and renders in one place.

The renderer does no layout: everything it draws is already positioned in the printout, so rendering a printout read back from a file gives the same PDF as rendering the one that was just built.

Types

type FontCheck

type FontCheck = build.FontCheck

FontCheck is what checking a template's fonts found out. See CheckFonts.

type Info

type Info struct {
	Name        string
	Description string
	Version     string
	Author      string
	// Page is the page size and margins, in points.
	Page printout.PageGeometry
	// Columns is the column count, 1 for a template with no columns node.
	Columns int
	// Names declared, in declaration order.
	Parameters []Parameter
	Variables  []string
	Fonts      []string
	Data       []string
	Groups     []string
	// Members are the record members the template declares,
	// empty for a template that declares none.
	Members []string
	// Subreports names the subreport nodes the template carries, in the
	// order they are reached, so a front end can say what a build will
	// pull in without loading anything itself.
	Subreports []string
}

Info is what a template says about itself.

It is what a front end needs to describe or check a template without building anything: the report's identification, its page geometry, and the names it declares. The engine reads none of it.

type Option

type Option func(*build.Options)

Option configures a build.

func AllowOverflow

func AllowOverflow() Option

AllowOverflow downgrades oversized-band errors to warnings recorded in the printout header, so an overflowing document is identifiable from the artifact.

func StrictFonts

func StrictFonts() Option

StrictFonts resolves only fonts the template names by file or data.

It fails with the unresolved typeface named. Tests run this way, so no outcome depends on what is installed.

func WithBuildTime

func WithBuildTime(when time.Time) Option

WithBuildTime fixes BUILD_TIME.

With it, and with StrictFonts, the same template over the same data produces a byte-identical printout on any machine.

func WithDiagnostics

func WithDiagnostics(fn func(string)) Option

WithDiagnostics receives messages about the machine: a font file that could not be read, two faces claiming one family and style.

They describe the host rather than the document, so they are not printout warnings.

func WithParam

func WithParam(name string, value any) Option

WithParam supplies a report parameter as a typed value.

Its type must match the parameter's declaration.

func WithTextParam

func WithTextParam(name, value string) Option

WithTextParam supplies a report parameter as text, parsed per the parameter's declared type -- the form the command line uses.

type Parameter

type Parameter struct {
	Name string
	// Type is the declared type: "string", "int", "decimal",
	// "float", "bool", "datetime", "date", "object" or "list".
	Type string
	// Default is the declared default text, absent for a parameter
	// that has none or computes one.
	Default string
	// HasDefault distinguishes a declared empty default from none.
	HasDefault bool
	// HasDefaultExpr reports a computed default.
	HasDefaultExpr bool
	// Required reports that the caller must supply a value.
	Required bool
	// Prompt is the template's hint that an interactive front end
	// should ask for this one. The engine ignores it.
	Prompt bool
}

Parameter is one declared parameter.

type Printout

type Printout = printout.Printout

Printout is the document a build produces. See package printout.

type Template

type Template struct {
	// contains filtered or unexported fields
}

Template is a loaded, validated template.

func LoadTemplate

func LoadTemplate(path string) (*Template, error)

LoadTemplate reads and validates a template file.

Validation runs once, here, before any data is read: node nesting, required properties, name collisions, geometry over-specification, content sources, and expression syntax are all load-time diagnostics rather than surprises mid-report.

func ParseTemplate

func ParseTemplate(name, source string) (*Template, error)

ParseTemplate reads a template from memory.

name is used in diagnostics and as the base for relative paths.

func (*Template) Build

func (tpl *Template) Build(records any, options ...Option) (*Printout, error)

Build applies the template to records.

records may be a []map[string]any or a slice of structs; struct fields map to declared members by name, or by an `sr:"..."` tag. Pass nil for a template with no records.

func (*Template) BuildJSON

func (tpl *Template) BuildJSON(reader io.Reader, options ...Option) (*Printout, error)

BuildJSON applies the template to records read from a JSON array document or from NDJSON.

The whole dataset is buffered: DATA_COUNT, report-scoped aggregates, and keep-together lookahead all need the full sequence.

func (*Template) CheckFonts

func (tpl *Template) CheckFonts(options ...Option) (*FontCheck, error)

CheckFonts resolves every font the template declares, without any data.

Loading a template validates everything a document can settle on its own. Font resolution is what it cannot: it reads the machine. So a check that wants to say more than "this file parses" runs this, which is what sr validate does.

A font that does not resolve is reported in the result rather than as an error, so that one missing typeface does not hide the next. Parameters are not required here -- one with no value and no default is simply left unbound.

It covers the templates a subreport names by file as well as this one: a template half of whose fonts resolve is not a template that resolves.

func (*Template) Info

func (tpl *Template) Info() Info

Info describes the loaded template.

func (*Template) Name

func (tpl *Template) Name() string

Name is the template's report name.

func (*Template) Warnings

func (tpl *Template) Warnings() []string

Warnings are load-time diagnostics that did not stop the template loading.

Directories

Path Synopsis
cmd
sr command
Command sr applies a report template to data and writes a PDF.
Command sr applies a report template to data and writes a PDF.
internal
barcodes
Package barcodes encodes a value into the stripe geometry a printout carries.
Package barcodes encodes a value into the stripe geometry a printout carries.
build
Package build is the layout engine.
Package build is the layout engine.
cli
Package cli is the command line, per doc/cli.md.
Package cli is the command line, per doc/cli.md.
data
Package data loads input records and coerces them to their declared types.
Package data loads input records and coerces them to their declared types.
fontres
Package fontres implements the font resolution chain, metrics, and word wrap of doc/template.md#font-resolution.
Package fontres implements the font resolution chain, metrics, and word wrap of doc/template.md#font-resolution.
geom
Package geom implements the geometry model of doc/template.md#geometry: dimensions with units, the three-decimal rounding rule, boxes, the any-two-of-three edge resolution, and content alignment.
Package geom implements the geometry model of doc/template.md#geometry: dimensions with units, the three-decimal rounding rule, boxes, the any-two-of-three edge resolution, and content alignment.
kdl
Package kdl reads a KDL v2 document into a small tree of its own.
Package kdl reads a KDL v2 document into a small tree of its own.
pdfscan
Package pdfscan reads a PDF back: its objects, its pages, and the text, paths and images its content streams draw.
Package pdfscan reads a PDF back: its objects, its pages, and the text, paths and images its content streams draw.
pdfw
Package pdfw writes PDF files: indirect objects, streams, the cross reference table, and the content stream operators a printout's marks need.
Package pdfw writes PDF files: indirect objects, streams, the cross reference table, and the content stream operators a printout's marks need.
segment
Package segment solves one-dimensional stretchable segment layout.
Package segment solves one-dimensional stretchable segment layout.
sfnt
Package sfnt reads the font tables an embedded PDF font needs and builds a subset containing only the glyphs a document uses.
Package sfnt reads the font tables an embedded PDF font needs and builds a subset containing only the glyphs a document uses.
tmpl
Package tmpl holds the template model of doc/template.md: the node tree a KDL document parses into, and the validation that runs once at load, before any data is read.
Package tmpl holds the template model of doc/template.md: the node tree a KDL document parses into, and the validation that runs once at load, before any data is read.
vars
Package vars implements variable accumulation: the twelve calc modes.
Package vars implements variable accumulation: the twelve calc modes.
Package meta holds build-time metadata stamped in by the linker.
Package meta holds build-time metadata stamped in by the linker.
Package pdf renders a printout as PDF.
Package pdf renders a printout as PDF.
Package printout is the document an engine produces and a renderer consumes: pages of absolutely-positioned marks in paint order.
Package printout is the document an engine produces and a renderer consumes: pages of absolutely-positioned marks in paint order.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL