blazon

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 10, 2026 License: MIT Imports: 13 Imported by: 0

README

blazon's own mark for its current release

blazon

The mark above is blazon's own version, drawn by blazon.
It changes with every release.

ci go reference MIT

Identicons for version numbers, in Go.

Not a general-purpose hash-to-picture library pointed at a version string. The version's structure is the input: major, minor and patch feed separate entropy streams, so how much two neighbouring releases resemble each other is a decision you make, not an accident of the hash.

And distinguishability is a test, not a claim. Every renderer is measured across a corpus of 184 versions, and a change that makes patch bumps harder to tell apart turns the build red.

No dependencies. Standard library only — including the rasteriser and the font. Those are the two things that decide what the pixels are, and a library whose whole premise is that a version always produces the same mark cannot outsource them: a dependency that sharpened its anti-aliasing would change every mark ever generated, arriving as a routine version bump. A test enforces it.

import "github.com/timzifer/blazon"

svg, err := blazon.SVG("1.4.2", blazon.Options{})
png, err := blazon.PNG("1.4.2", blazon.Options{Renderer: "flowfield", Size: 512})
txt, err := blazon.Text("1.4.2", blazon.Options{Renderer: "bishop"})
go install github.com/timzifer/blazon/cmd/blazon@latest

blazon 1.4.2                        # terminal
blazon 1.4.2 -r flowfield -o x.svg
blazon dist -r superformula         # the distance histogram behind the thresholds

How it works

Three stages. Each one is a seam you can cut at.

1 — Entropy. Not sha256("1.4.2") in one piece. Four separated streams:

h0 = hash(major)                      archetype
h1 = hash(major, minor)               variant
h2 = hash(major, minor, patch)        detail
h3 = hash(full version)               prerelease

Fields are length-prefixed, so 1.23.4 and 12.3.4 cannot collide.

2 — Parameter mapping. A Renderer draws its parameters from those streams and decides what the mark means. This is the stage that determines whether the library is useful. Not every renderer hashes: cistercian, dial and orbit encode the number itself, so the version can be read back out of the mark. Those are marked Ordered and are held to uniqueness rather than to the distance thresholds — being systematic is the point of them.

3 — Render primitives. A canvas.Canvas turns drawing calls into SVG, pixels or terminal cells. This stage decides only how it looks. Every renderer works on every back end.

An API choice, not a design decision:

Policy Effect
PolicyFamilyAmplified (default) Major sets the archetype, minor the variant, patch the detail — and each renderer routes two or three of its highest-contrast parameters through the patch stream, so adjacent patches stay in the family but are never hard to tell apart.
PolicyFamily The plain cascade. Patch changes only fine detail.
PolicyIndependent Every stream comes from the full version. Neighbours look unrelated: maximum distinguishability, no family information.

A prerelease never changes the core streams: 1.0.0-rc.1 still reads as a 1.0.0, marked by a small band of dots.

The testable invariant

This is the part that separates blazon from any identicon port.

Marks are rendered across a version corpus, reduced to a 128-bit perceptual hash — a DCT hash plus a difference hash, because either alone confuses shapes the other separates — and the Hamming distances between related versions are held to per-renderer thresholds:

blazontest.Suite{
    Renderer:   myRenderer,
    Thresholds: blazontest.Thresholds{MinPatch: 6, MinMinor: 9, MinMajor: 20, Floor: 2},
}.Run(t)

The suite checks:

  • patch, minor and major neighbours clear their minimum distances;
  • no pair anywhere in the corpus falls below the global floor;
  • family cohesion — versions sharing a major really are closer to each other than to other majors, so the family policy is not a lie;
  • prereleases stay close to their release, bounded from above rather than below;
  • uniqueness — no two versions render to the same bytes, exactly rather than perceptually;
  • determinism — the same version renders identically, byte for byte where a renderer declares it can.

Measurement is monochrome on purpose. A luminance-based hash scores two marks differing only in hue as identical, so measuring in colour would credit colour for distinctness the form does not have. Judging form alone makes the guarantee hold in print, in a monochrome terminal, and for a colour-blind reader.

Thresholds are calibrated from each renderer's own histogram, not guessed:

blazon dist -r truchet

Renderers

Name What it is
superformula Gielis' superformula, drawn from curated shape families — star, flower, gear, crystal, leaf, shield. The default.
flowfield Streamlines through a Sherlock–Monro orientation field with fingerprint cores and deltas. Arch, loop and whorl come from the major version; minutiae emerge from the ridge spacing rule.
truchet Truchet tiles. Arcs meet at cell edge midpoints, so the marks join across the grid into loops and labyrinths.
polar A bitmap in rings and sectors instead of rows and columns, mirrored across a fold from the archetype stream, with a centre device in the hole and a radial profile that varies ring by ring.
cistercian Medieval numerals: one stave per version component, four digits per stave. Legible — you can read the version back out of it.
dial One quadrant per component, the value written in binary as radial bands. The separator is the angle: the spokes are the dots of the version string.
orbit One ring per component, major innermost, the value written in binary as cells running clockwise from noon. The separator is the radius.
bishop The drunken bishop walk from OpenSSH randomart, for the terminal.

Output

  • SVG — plain string building, deterministic number formatting.
  • PNG — anti-aliased by exact area coverage: each edge deposits the signed area it sweeps per pixel column, and a running sum along the row gives coverage. Filling a shape and summing the alpha returns its geometric area, which is how the anti-aliasing is tested.
  • Terminal — half-block characters give each cell two square pixels, so vector renderers arrive as pictures rather than as ASCII art. Falls back to a density ramp under NO_COLOR.

Colour is derived in OKLCH with gamut mapping, held in a lightness band that keeps contrast on both a white page and a dark terminal.

Generated by blazon gallery and verified in CI: if the committed images stop matching what the code renders, the build fails.

bishop

Terminal renderer on a 17×10 character grid.

bishop across a version corpus

Family: 1.0.0 → 1.0.1 → 1.0.2 → 1.1.0 → 2.0.0

bishop family strip

cistercian

cistercian across a version corpus

Family: 1.0.0 → 1.0.1 → 1.0.2 → 1.1.0 → 2.0.0

cistercian family strip

dial

dial across a version corpus

Family: 1.0.0 → 1.0.1 → 1.0.2 → 1.1.0 → 2.0.0

dial family strip

flowfield

flowfield across a version corpus

Family: 1.0.0 → 1.0.1 → 1.0.2 → 1.1.0 → 2.0.0

flowfield family strip

orbit

orbit across a version corpus

Family: 1.0.0 → 1.0.1 → 1.0.2 → 1.1.0 → 2.0.0

orbit family strip

polar

polar across a version corpus

Family: 1.0.0 → 1.0.1 → 1.0.2 → 1.1.0 → 2.0.0

polar family strip

superformula (default)

superformula across a version corpus

Family: 1.0.0 → 1.0.1 → 1.0.2 → 1.1.0 → 2.0.0

superformula family strip

truchet

truchet across a version corpus

Family: 1.0.0 → 1.0.1 → 1.0.2 → 1.1.0 → 2.0.0

truchet family strip

Contributing

A new renderer needs an entry in the threshold table and has to pass blazontest.Suite; see CONTRIBUTING.md for how to calibrate one.

License

MIT.

Documentation

Overview

Package blazon turns a version number into a visual seal.

The pipeline has three stages. Entropy is derived from the version in separated streams (Seed, Params) so that family resemblance between neighbouring versions is a policy choice rather than an accident. A Renderer maps those streams onto its own parameters — this is the stage that decides whether the result is useful. A canvas.Canvas turns drawing primitives into SVG, pixels or terminal cells — this stage decides only how it looks.

Distinguishability is not a promise here but a test: see the blazontest package, which measures perceptual distance across a version corpus and fails when neighbouring versions render too much alike.

Index

Constants

View Source
const DefaultRenderer = "superformula"

DefaultRenderer is used when Options.Renderer is empty.

View Source
const LogoRenderer = "flowfield"

LogoRenderer draws the mark blazon uses for itself.

View Source
const Release = "0.2.0"

Release is this library's own version.

It is here because blazon signs its own releases with its own marks: the logo in the README is the flowfield mark for this exact version, and the release workflow refuses a tag that does not match. Bump it in the same commit as the tag.

Variables

View Source
var (
	// ErrEmptyVersion is returned when the input contains no version at all.
	ErrEmptyVersion = errors.New("blazon: empty version string")
	// ErrSyntax is returned when the input is not a valid semantic version.
	ErrSyntax = errors.New("blazon: invalid semantic version")
)

Functions

func EncodePNG

func EncodePNG(img image.Image) ([]byte, error)

EncodePNG encodes an image as PNG.

The encoder is the library's own, for the same reason the rasteriser and the font are: it is the last stage that decides what bytes a version turns into, and importing image/png would hand that decision — and about a fifth of a megabyte of binary — to something outside this repository.

The error is retained in the signature because callers already handle one and encoding is where a future format could fail; today it is always nil.

func Image

func Image(version string, o Options) (*image.RGBA, error)

Image renders a version to an RGBA image.

func ImageWith

func ImageWith(r Renderer, version string, o Options) (*image.RGBA, error)

ImageWith renders with an explicit renderer, bypassing the registry.

func MarkPrerelease

func MarkPrerelease(ctx *Context)

MarkPrerelease draws the prerelease marker: a short row of dots along the bottom edge of the drawing box, their count and spacing taken from the prerelease entropy stream.

The marker is deliberately small. A prerelease must still read as the release it precedes — 1.0.0-rc.1 is a 1.0.0 — so it may not disturb the main shape. But it must exist: without it, 1.0.0 and 1.0.0-rc.1 would render to identical bytes, and two different versions sharing one mark is the one thing an identicon may never do.

It lives here rather than in each renderer so that the rule is the same everywhere and a new renderer gets it by calling one function.

func MarkPrereleaseCells

func MarkPrereleaseCells(ctx *Context, cols, row int)

MarkPrereleaseCells is the character-grid form of the marker: a run of dots in a row the renderer has reserved for it. Text renderers call this instead of MarkPrerelease, which draws vector geometry a character canvas would have to rasterise.

The row must be reserved rather than borrowed. Overwriting cells the renderer has already drawn would hide part of the mark itself, and a marker that destroys information is worse than no marker.

func PNG

func PNG(version string, o Options) ([]byte, error)

PNG renders a version to a PNG file.

func Register

func Register(r Renderer)

Register adds a renderer under its own name. It panics on a duplicate name, because two renderers sharing a name would make golden files and gallery output ambiguous. Renderers in this package register themselves; Register is exported so that callers can add their own and run them through blazontest.

func Renderers

func Renderers() []string

Renderers lists the registered renderer names in sorted order.

func SVG

func SVG(version string, o Options) ([]byte, error)

SVG renders a version to an SVG document.

func SVGWith

func SVGWith(r Renderer, version string, o Options) ([]byte, error)

SVGWith renders with an explicit renderer, bypassing the registry. It is how blazontest exercises a renderer that a caller has written but not registered.

func Text

func Text(version string, o Options) (string, error)

Text renders a version as a block of terminal output.

Half-block characters give each cell two square pixels, so vector renderers arrive in the terminal as pictures rather than as ASCII art. Renderers that declare their own character grid — the randomart field — write glyphs directly instead.

func TextWith

func TextWith(r Renderer, version string, o Options) (string, error)

TextWith renders with an explicit renderer, bypassing the registry.

Types

type Background

type Background int

Background selects the ground the mark is drawn on.

const (
	// BackgroundTransparent leaves the ground unpainted. Ink lightness is
	// held mid-band so the mark reads on light and dark surfaces alike.
	BackgroundTransparent Background = iota
	BackgroundLight
	BackgroundDark
)

type BitReader

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

BitReader is a deterministic, unlimited bit stream derived from a 32-byte hash. Bits are consumed most-significant-first from successive bytes; when the current block is exhausted it is extended by hashing the root together with a counter, so a renderer never has to budget its bits.

A BitReader is not safe for concurrent use.

func NewBitReader

func NewBitReader(root [32]byte) *BitReader

NewBitReader starts a stream at the given root hash.

func (*BitReader) Bit

func (b *BitReader) Bit() uint64

Bit returns the next single bit.

func (*BitReader) Bool

func (b *BitReader) Bool() bool

Bool returns the next bit as a boolean.

func (*BitReader) Choice

func (b *BitReader) Choice(n int) int

Choice returns an index in [0,n).

func (*BitReader) Derive

func (b *BitReader) Derive(tag string) *BitReader

Derive returns an independent stream from the same root, separated by tag. Use it when a renderer needs a fresh draw that must stay deterministic, such as resampling a degenerate parameter set.

func (*BitReader) Float01

func (b *BitReader) Float01() float64

Float01 returns a value in [0,1) with 53 bits of precision.

func (*BitReader) IntRange

func (b *BitReader) IntRange(lo, hi int) int

IntRange returns an integer in [lo,hi] inclusive. It uses rejection sampling so the result is unbiased regardless of range width.

func (*BitReader) LogRange

func (b *BitReader) LogRange(lo, hi float64) float64

LogRange returns a value in [lo,hi) distributed uniformly on a logarithmic scale. Both bounds must be positive. Shape parameters whose visual effect is multiplicative — the superformula exponents, for instance — must be drawn this way, otherwise nearly every sample lands in the large-value regime and the shapes collapse onto one another.

func (*BitReader) Range

func (b *BitReader) Range(lo, hi float64) float64

Range returns a value in [lo,hi).

func (*BitReader) Sign

func (b *BitReader) Sign() float64

Sign returns -1 or +1.

func (*BitReader) Uint

func (b *BitReader) Uint(n uint) uint64

Uint returns the next n bits as an unsigned integer, most-significant bit first. n must be in [0, 64].

type Box

type Box struct{ X, Y, W, H float64 }

Box is the drawing area available to a renderer, in canvas units, after padding has been applied.

func (Box) Center

func (b Box) Center() (x, y float64)

Center returns the middle of the box.

func (Box) Min

func (b Box) Min() float64

Min returns the shorter of the box's two sides.

type Caps

type Caps struct {
	// TextOnly means the renderer draws exclusively through Canvas.Cell.
	// Vector back ends still render it, via a monospace font, but the
	// terminal is its native target.
	TextOnly bool

	// GridCols and GridRows are the character grid a TextOnly renderer needs.
	GridCols, GridRows int

	// ByteExact means the renderer's geometry is derived from integer
	// arithmetic only, so its output can be compared against golden files
	// byte for byte. Renderers using transcendental functions cannot promise
	// this: math.Sin and math.Pow may differ by an ulp between architectures,
	// so those are compared by perceptual hash with a tolerance instead.
	ByteExact bool

	// Ordered means the mark encodes the version legibly rather than
	// hashing it. Such renderers are exempt from the distinctness thresholds,
	// because being systematic is the point; they are checked for injectivity
	// instead.
	Ordered bool
}

Caps describes what a renderer can and cannot do. The registry and the test suite consult it instead of special-casing renderers by name.

type Context

type Context struct {
	Params  *Params
	Canvas  canvas.Canvas
	Palette Palette
	Box     Box
	Options Options
}

Context is everything a renderer is given. Palette and Box are resolved centrally so that colour policy and padding stay in one place instead of being reimplemented by every renderer.

type Options

type Options struct {
	// Renderer names a registered renderer; empty means DefaultRenderer.
	Renderer string
	// Size is the edge length in pixels or user units; zero means 512.
	Size int
	// Padding is the fraction of Size kept clear at each edge. Zero means the
	// default of 0.06; use a negative value for no padding at all.
	Padding float64
	// Policy controls family resemblance between neighbouring versions.
	Policy Policy
	// Palette selects colour or monochrome ink.
	Palette PaletteMode
	// Background selects the ground.
	Background Background
	// Cols is the terminal width in character cells for Text output; zero
	// means 32. It is ignored by renderers that declare their own grid.
	Cols int
	// NoColor suppresses ANSI colour in Text output, falling back to a
	// density ramp.
	NoColor bool
}

Options configures a render. The zero value is valid and yields a 512-pixel mark with the default renderer, the default policy and a transparent ground.

type Palette

type Palette struct {
	Ground color.RGBA // ignored when the background is transparent
	Inks   []color.RGBA
}

Palette is the resolved set of colours for one mark.

func DerivePalette

func DerivePalette(p *Params, mode PaletteMode, bg Background) Palette

DerivePalette resolves the palette for a render. The base hue comes from the archetype stream, so a major version keeps its colour identity; the hue rotation is an amplified parameter, which is what makes neighbouring patch versions separate at a glance even when their geometry barely moves.

func (Palette) Ink

func (p Palette) Ink(i int) color.RGBA

Ink returns the i-th ink, cycling. Renderers index inks rather than picking colours themselves, which keeps colour policy in one place.

type PaletteMode

type PaletteMode int

PaletteMode selects how colour is derived.

const (
	// PaletteColor derives hues from the version's entropy.
	PaletteColor PaletteMode = iota
	// PaletteMono renders in a single neutral ink. Useful for print, for
	// embedding in coloured UI, and for perceptual-hash comparisons that
	// should judge form rather than hue.
	PaletteMono
)

type Params

type Params struct {
	Seed   *Seed
	Policy Policy
	// contains filtered or unexported fields
}

Params is what a renderer draws its parameters from. Each named stream is a separate, lazily created BitReader: draws from one never shift the bits seen by another, so a renderer can add a parameter to one stream without disturbing the rest of the image.

func NewParams

func NewParams(s *Seed, p Policy) *Params

NewParams binds a seed to a policy.

func (*Params) Amplified

func (p *Params) Amplified() *BitReader

Amplified is the stream a renderer uses for its two or three highest-contrast parameters. Under the default policy it follows the patch; under PolicyFamily it follows the minor, so patch bumps stay subtle. Drawing from it rather than branching on Policy keeps renderers policy-agnostic.

func (*Params) Archetype

func (p *Params) Archetype() *BitReader

Archetype carries the coarsest identity: major only. Parameters that define the basic gestalt belong here.

func (*Params) Detail

func (p *Params) Detail() *BitReader

Detail carries the full core version, so it changes on every patch bump.

func (*Params) PaletteBase

func (p *Params) PaletteBase() *BitReader

PaletteBase and PaletteShift are the streams the palette draws from. They are separate from the renderer streams on purpose: if colour shared the archetype stream, switching to monochrome would shift every subsequent draw and change the shape of the mark. Form must not depend on whether the mark is being rendered in colour.

func (*Params) PaletteShift

func (p *Params) PaletteShift() *BitReader

PaletteShift follows the amplified stream, so hue moves with the patch under the default policy.

func (*Params) Pre

func (p *Params) Pre() *BitReader

Pre carries prerelease and build metadata. Renderers use it for a marker rather than for the main shape, so 1.0.0-rc1 stays visibly a 1.0.0.

func (*Params) Reset

func (p *Params) Reset()

Reset discards all drawn bits, so the same Params can render again identically.

func (*Params) Variant

func (p *Params) Variant() *BitReader

Variant carries major and minor: the shape family's variation.

func (*Params) Version

func (p *Params) Version() Version

Version is the version being rendered. Renderers that encode the number itself rather than its hash — cistercian — read it directly.

type Policy

type Policy int

Policy decides how much of a version's identity is inherited from its neighbours. It is an API-level choice, not a design detail: it determines whether adjacent versions read as a family with a mutation, or as unrelated marks.

const (
	// PolicyFamilyAmplified is the default. Major fixes the archetype, minor
	// the variant, patch the detail — but a renderer additionally routes two
	// or three of its highest-contrast parameters (rotation, hue, one shape
	// parameter) through the patch stream. Neighbouring patches stay
	// recognisably related while remaining easy to tell apart at a glance.
	PolicyFamilyAmplified Policy = iota

	// PolicyFamily is the plain cascade. Patch changes only fine detail;
	// high-contrast parameters follow the minor stream.
	PolicyFamily

	// PolicyIndependent derives every stream from the full version. Adjacent
	// versions look unrelated: maximum distinguishability, no family
	// information.
	PolicyIndependent
)

func (Policy) String

func (p Policy) String() string

type Renderer

type Renderer interface {
	Name() string
	Caps() Caps
	Render(ctx *Context) error
}

Renderer maps entropy onto geometry — stage two of the pipeline.

func Lookup

func Lookup(name string) (Renderer, bool)

Lookup returns the renderer registered under name.

type Seed

type Seed struct {
	V Version

	H0 [32]byte // archetype: major
	H1 [32]byte // variant:   major, minor
	H2 [32]byte // detail:    major, minor, patch
	H3 [32]byte // prerelease: full version including build metadata
}

Seed holds the four separated entropy streams for a version. Splitting the hash by version component is what makes family resemblance controllable; hashing the whole string at once would not.

func NewSeed

func NewSeed(v Version) *Seed

NewSeed derives the entropy streams for v.

type Version

type Version struct {
	Major      uint64
	Minor      uint64
	Patch      uint64
	Prerelease string // without the leading '-'
	Build      string // without the leading '+'
}

Version is a parsed semantic version. Build metadata is retained because it feeds the prerelease entropy stream, even though SemVer ignores it for ordering.

func ParseVersion

func ParseVersion(s string) (Version, error)

ParseVersion parses a semantic version string. A single leading "v" is tolerated because Go module tags carry one. Missing minor and patch components default to zero, so "1" and "1.2" are accepted as "1.0.0" and "1.2.0" respectively.

func (Version) Core

func (v Version) Core() Version

Core reports the version without prerelease or build metadata.

func (Version) IsPrerelease

func (v Version) IsPrerelease() bool

IsPrerelease reports whether the version carries a prerelease identifier.

func (Version) String

func (v Version) String() string

String renders the canonical SemVer form.

Directories

Path Synopsis
Package blazontest measures whether a renderer actually distinguishes versions, and fails the build when it does not.
Package blazontest measures whether a renderer actually distinguishes versions, and fails the build when it does not.
Package canvas holds the render primitives — stage three of the pipeline.
Package canvas holds the render primitives — stage three of the pipeline.
cmd
blazon command
Command blazon renders version numbers as visual seals, and provides the tools used to judge whether a renderer actually distinguishes them.
Command blazon renders version numbers as visual seals, and provides the tools used to judge whether a renderer actually distinguishes them.
internal
noise
Package noise provides deterministic 2D simplex noise.
Package noise provides deterministic 2D simplex noise.
phash
Package phash computes perceptual hashes of rendered marks.
Package phash computes perceptual hashes of rendered marks.
pngenc
Package pngenc encodes images as PNG without image/png.
Package pngenc encodes images as PNG without image/png.
sha256
Package sha256 implements SHA-256 (FIPS 180-4).
Package sha256 implements SHA-256 (FIPS 180-4).
sizeprobe command
Command sizeprobe renders one mark and writes it to standard output.
Command sizeprobe renders one mark and writes it to standard output.

Jump to

Keyboard shortcuts

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