unpixel

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Jun 19, 2026 License: GPL-3.0 Imports: 14 Imported by: 0

README

UnPixel

A faithful pure-Go port of Bishop Fox's unredacter — reconstructs text hidden behind mosaic pixelation or Gaussian blur redaction. Background: Never use pixelation to redact text.

CI Go Reference Go Report Card Go 1.26 License GPL-3.0-or-later

Status: the library and CLI are usable (~90% test coverage, all gates green). See PROGRESS.md for the roadmap.

Table of contents

How it works

Pixelation is a deterministic function of its input, so UnPixel doesn't "un-blur" anything — it runs generate-and-test. It renders a candidate string in the target font, re-pixelates it with the same block grid as the redacted region, scores the image distance against the target, and drives a guided depth-first search over candidate strings: discover the grid offset, then extend the plaintext character by character, pruning branches as soon as the re-pixelated output stops matching. Because pixelation averages each block, only the true text reproduces the target's blocks exactly.

See docs/DESIGN.md for the algorithm and the library choices behind it.

Features

  • Pure Go, no CGO. Deterministic, statically linked, cross-compilable — no C toolchain.
  • Library-agnostic progress API. Run streams typed Progress events on a buffered channel (best guess, current candidate, score, depth, offsets probed/total, evaluated count, elapsed), so any UI — web/SSE, TUI, desktop — can subscribe via the channel or the OnProgress callback.
  • Pluggable everything. Swap the Renderer, Pixelator, Metric, or search Strategy through Config; the faithful defaults are wired by importing the defaults package. Built-in choices: guided-DFS or beam search; pixelmatch or SSIM (structural) distance.
  • Concurrent by default. Grid-offset discovery and per-offset search fan out across Config.Workers goroutines (default: all CPUs) with a deterministic merge — same output regardless of scheduling. ~4× faster offset discovery on a typical laptop.
  • Auto-detects the block size. Leave Config.BlockSize unset and New infers the mosaic grid from the image (InferBlockSize), so callers don't have to measure it.
  • Blur, not just mosaic. Blur is also a deterministic function of its input, so the same attack applies: pixelate.NewGaussianBlur(σ) / WithPixelator reproduce a Gaussian blur, and the CLI --redaction auto|mosaic|blur auto-detects it and estimates σ (InferBlurSigma) — so a blurred secret recovers zero-config, no σ or font supplied.
  • Zero-config font matching. Recovery needs the redaction's typeface — so with no --font, UnPixel sweeps a built-in set of redistributable fonts (Liberation Sans/Serif/Mono ≈ Arial/Times/Courier, Carlito ≈ Calibri, Caladea ≈ Cambria, Source Code Pro & JetBrains Mono for code) in parallel and keeps the best fit by whole-image score. Or match it yourself with --font/--font-size/--letter-spacing (and sweep your own via repeated --font/--font-dir). Library: the fonts bundle + RecoverMultiFont; render.NewXImageFromFonts for a custom face.
  • Ranked results, not just one guess. Each Result carries the top-N candidates per grid offset (sorted by score, ties broken deterministically) plus Confidence/Ambiguity and a whole-image BestTotal distance — comparable across runs, so it can rank fonts or styles — letting callers surface alternatives instead of a single best guess.
  • Self-consistent correctness. Fidelity is judged by a redaction round-trip (redact a known plaintext, then recover it). Matching a Chromium-rendered redaction is a documented Phase-2 goal (needs a chromedp renderer).
  • ~90% test coverage across rendering, pixelation, metrics, search, CLI, and end-to-end.

Install

As a library:

go get github.com/oioio-space/unpixel

As a command-line tool:

go install github.com/oioio-space/unpixel/cmd/unpixel@latest

From source (development) — uses mise for the toolchain and tasks:

git clone https://github.com/oioio-space/unpixel.git
cd unpixel
mise run setup     # install pinned tools + wire git hooks
mise run test      # verify the build
mise run           # list all tasks

Requires Go 1.26+.

Command-line tool

unpixel redacted.png                       # zero-config: sweeps the built-in fonts; best guess to stdout
cat redacted.png | unpixel -               # read PNG from stdin
unpixel --format json --top 10 redacted.png
unpixel --strategy beam --metric ssim --workers 8 redacted.png

# Match a known typeface yourself (skips the sweep) — e.g. a Consolas code screenshot:
unpixel --font Consolas.ttf --font-size 24 --letter-spacing -0.2 -b 5 redacted.png

# Sweep your own candidate fonts (or a whole directory) instead of the built-ins:
unpixel --font Arial.ttf --font Consolas.ttf --font Courier.ttf -b 5 redacted.png
unpixel --font-dir /usr/share/fonts/truetype -b 5 redacted.png

Key flags (unpixel --help for the full list):

Flag Default Purpose
--charset a–z + space Candidate characters to try
--charset-preset Named charset when --charset is unset: lower, alnum, ascii/code
--block-size, -b 0 (auto) Pixelation block size; 0 auto-detects from the image
--font embedded (Liberation Sans) TTF/OTF font to render candidates; repeat to sweep and keep the best fit
--font-dir Directory of TTF/OTF fonts to sweep (each tried; best whole-image fit wins)
--font-size 0 (32) Font size in points to match the redaction
--letter-spacing 0 Extra px after each glyph, like CSS letter-spacing (may be negative)
--strategy guided guided (full DFS) or beam (bounded, faster)
--beam-width 0 (16) Candidates kept per depth level under --strategy beam
--metric pixelmatch pixelmatch (faithful) or ssim (structural)
--workers 0 (all CPUs) Grid offsets searched concurrently; also the sweep's core budget
--top, -n 5 Ranked candidates to report
--format, -f text text or machine-readable json
--timeout 0 (none) Max recovery time

The best guess prints to stdout (so it pipes cleanly); the ranked table and live progress go to stderr. --format json emits a stable schema (best_guess, confidence, total_score, top, and a ranked fonts array when sweeping).

Tip: lower block sizes carry less information per glyph, so a tighter --threshold (e.g. 0.1) prunes coincidental matches and lets the whole-image score pick the complete answer.

Quick start

One call — give it an image (or a path), get the text back:

package main

import (
	"context"
	"fmt"

	"github.com/oioio-space/unpixel"
	_ "github.com/oioio-space/unpixel/defaults" // wires the default renderer/pixelator/metric/strategy
)

func main() {
	res, err := unpixel.RecoverFile(context.Background(), "redacted.png")
	if err != nil {
		panic(err)
	}
	fmt.Println("recovered:", res.BestGuess)
}

Recover / RecoverReader / RecoverFile take functional options for the common knobs while auto-detecting the rest (block size, …):

res, err := unpixel.Recover(ctx, img,
	unpixel.WithCharset("abcdefghijklmnopqrstuvwxyz0123456789 "),
	unpixel.WithWorkers(8),
)

Unknown typeface? Build a renderer per candidate font (you supply the .ttf) and let RecoverMultiFont recover with each in parallel, ranked best-fit first:

import "github.com/oioio-space/unpixel/defaults"

var rs []unpixel.Renderer
for _, data := range fontFiles { // each is TTF/OTF bytes you read
	r, err := defaults.RendererFromFonts(data, nil)
	if err != nil {
		panic(err)
	}
	rs = append(rs, r)
}
ranked, err := unpixel.RecoverMultiFont(ctx, img, rs, unpixel.WithBlockSize(5))
best := ranked[0] // lowest BestTotal — the font that fit best
fmt.Printf("%q via font #%d\n", best.Result.BestGuess, best.Index)

For streaming progress or full control, drop to the low-level Engine (the helpers wrap exactly this):

Low-level Engine API
eng, err := unpixel.New(img, unpixel.Config{}) // zero Config = faithful defaults
if err != nil {
	panic(err)
}
progress, results := eng.Run(context.Background())
go unpixel.OnProgress(progress, func(p unpixel.Progress) {
	fmt.Printf("\rbest: %-20q (%.3f)", p.BestGuess, p.BestScore)
})
fmt.Println("\nrecovered:", (<-results).BestGuess)

Public API (root package unpixel):

Symbol Purpose
Recover(ctx, image.Image, ...Option) (Result, error) One call: search and return the best result
RecoverReader(ctx, io.Reader, ...Option) / RecoverFile(ctx, path, ...Option) Decode then Recover
RecoverMultiFont(ctx, image.Image, []Renderer, ...Option) ([]FontResult, error) Sweep candidate fonts in parallel; results ranked best-fit first by BestTotal
With* options (WithCharset, WithWorkers, WithRenderer, WithStrategy, …) Tweak the common knobs; WithConfig seeds a full Config
New(redacted image.Image, cfg Config) (*Engine, error) Build an engine; zero Config = faithful defaults
(*Engine).Run(ctx) (<-chan Progress, <-chan Result) Run the search; stream progress, deliver the result
(*Engine).Config() Config Resolved config (e.g. the inferred block size)
OnProgress(ch <-chan Progress, fn func(Progress)) Drain progress events into a callback (any UI)
InferBlockSize(image.Image) int Detect the mosaic block size
Renderer, Pixelator, Metric, Strategy Pluggable pipeline interfaces
Config, Style, Result, FontResult, Eval, Offset, Progress, EventKind Configuration and result/event types

Configuration

Pass a Config to unpixel.New. Every zero value falls back to a documented default.

Field Type Default Meaning
Charset string "abcdefghijklmnopqrstuvwxyz " Candidate characters to search
MaxLength int 20 Maximum plaintext length
BlockSize int 0 → auto / 8 Pixelation block size; ≤0 auto-detects via InferBlockSize, else falls back to 8
Threshold float64 0.25 Max image-distance score (0–1) to keep a candidate
SpaceThreshold float64 0.5 Looser threshold for extending with a space (whitespace blur)
ThresholdFor func(rune) float64 space→SpaceThreshold, else Threshold Per-character threshold; override for new char classes
TopN int 5 Ranked candidates kept per offset in Result.TopN
Style Style Liberation Sans, 32 px, white bg Font size/weight/padding and LetterSpacing for rendering (the font itself comes from the Renderer)
Renderer Renderer x/image/font (pure Go) Text → raster
Pixelator Pixelator block-average Raster → pixelated
Metric Metric orisano/pixelmatch Image-distance score
Strategy Strategy guided DFS Candidate-space search (defaults.GuidedStrategy() / defaults.BeamStrategy(width))
BeamWidth int 16 Candidates kept per depth level — beam strategy only
CacheSize int 4096 LRU size for prefix-render memoization — beam strategy only (0 disables)
Workers int 0 → all CPUs Grid offsets probed/searched concurrently; 1 forces sequential. Never changes output

Selecting beam search (bounded branching + prefix-render caching) instead of the default DFS:

cfg := unpixel.Config{Strategy: defaults.BeamStrategy(0)} // 0 = use BeamWidth (default 16)

Architecture

Package layout
github.com/oioio-space/unpixel
├── unpixel.go              # Engine, Config, Result, Eval, Offset, Progress; the 4 interfaces
├── defaults/               # wires the default components (breaks the root↔internal import cycle)
├── fonts/                  # bundled redistributable fonts (OFL/Apache) for the zero-config sweep
├── internal/
│   ├── imutil/             # crop / pad / compose; blueMargin & leftEdge detection
│   ├── pixelate/           # block-average pixelator; grid-origin crop; white padding
│   ├── metric/             # pixelmatch (faithful default) + simple RGB metric
│   ├── render/             # pure-Go x/image/font renderer (embedded or custom fonts; letter-spacing)
│   │   └── fonts/          #   embedded Liberation Sans (Regular/Bold) + OFL license
│   └── search/             # offset discovery + marginal cropping + guided DFS + whole-image ranking
└── cmd/unpixel/            # CLI (urfave/cli/v3): recovery, font sweep, text/JSON output

The four interfaces (Renderer, Pixelator, Metric, Strategy) live in the root package so they can be implemented and injected from outside; the concrete implementations stay under internal/.

Rendering fidelity & Phase-2 roadmap

The original unredacter rasterizes via Chromium; UnPixel uses pure-Go golang.org/x/image/font. Byte-identical glyphs across engines aren't possible (different hinting/anti-aliasing), so correctness is judged by self-consistency: redact a known plaintext with UnPixel's own renderer, then recover it. Recovering a Chromium-produced redaction (e.g. the original secret.png) is a Phase-2 goal requiring a chromedp renderer.

Landed: top-N confidence/ambiguity reporting; a beam-search strategy with prefix-render memoization (defaults.BeamStrategy); an SSIM metric (defaults.SSIMMetric); automatic block-size inference (InferBlockSize); and goroutine fan-out over offset discovery and per-offset search (Config.Workers, deterministic merge). Still ahead (behind the interfaces; the faithful default stays put): edge-aware metrics and the chromedp fidelity renderer (deferred — it would require a Chrome binary at runtime/CI). Details in docs/DESIGN.md § Phase-2.

Contributing

mise run test:watch   # TDD loop
mise run lint         # gofmt + go vet + golangci-lint
mise run test         # tests
mise run cover        # coverage report (floor: 85%)
mise run bench        # hot-path benchmarks (benchstat-proven)
mise run gen          # regenerate test fixtures (mise run gen:check verifies no drift)
mise run ci           # everything CI runs

Commits go through git hooks — artifacts → secrets (gitleaks) → vulns (gosec + govulncheck) → style (golangci-lint) → cgo:check — plus a /simplify review. CI re-runs all of it and adds a CycloneDX SBOM scanned by grype, a full-history secret scan, and the coverage floor.

Two absolute project rules: the project stays pure Go (no CGO), and the hot path is benchmarked with benchstat-proven changes. See CLAUDE.md for tooling and PROGRESS.md for the roadmap.

Credits

License

GPL-3.0-or-later — see LICENSE. UnPixel is a derivative work of Bishop Fox's unredacter (GPL-3.0); the copyleft license is preserved. © the UnPixel authors; original © Bishop Fox.

Documentation

Overview

Package unpixel reconstructs text hidden behind pixelation.

UnPixel attempts to recover the original text from a pixelated (mosaic-blurred) image region. The algorithm works by rendering each candidate string into an off-screen image, re-applying the same block-average pixelation with the same grid origin, and measuring the pixel-level distance against the redacted region. A guided depth-first search explores the candidate space character by character, pruning branches whose cumulative score exceeds the configured threshold.

Usage

Obtain or synthesise an image.Image containing the pixelated region, construct an Engine with New, and call Run. Import the defaults package for its side-effect to wire the standard renderer, pixelator, metric, and search strategy; without it the components must be supplied explicitly in Config.

import _ "github.com/oioio-space/unpixel/defaults" // wire default components

eng, err := unpixel.New(img, unpixel.Config{})
if err != nil { ... }
progCh, resCh := eng.Run(ctx)
unpixel.OnProgress(progCh, func(p unpixel.Progress) {
    fmt.Println(p.BestGuess)
})
for r := range resCh {
    fmt.Println(r.BestGuess, r.BestScore)
}

UnPixel is a faithful Go port of Bishop Fox's unredacter (GPL-3.0).

Example

Example demonstrates the quick-start usage of UnPixel: construct an Engine from a pixelated image, run the search, consume progress events, then collect results. The image here is a small synthetic white rectangle used purely to keep the example self-contained and fast; in practice, supply the actual pixelated screenshot region.

package main

import (
	"context"
	"fmt"
	"image"
	"image/color"

	"github.com/oioio-space/unpixel"
	_ "github.com/oioio-space/unpixel/defaults"
)

func main() {
	// Build a tiny synthetic image to stand in for a real pixelated screenshot.
	img := image.NewRGBA(image.Rect(0, 0, 64, 16))
	for y := range img.Bounds().Dy() {
		for x := range img.Bounds().Dx() {
			img.SetRGBA(x, y, color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff})
		}
	}

	eng, err := unpixel.New(img, unpixel.Config{
		Charset:   "ab ",
		MaxLength: 2,
		BlockSize: unpixel.DefaultBlockSize,
	})
	if err != nil {
		fmt.Println("new:", err)
		return
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	progCh, resCh := eng.Run(ctx)

	// Consume progress events concurrently while collecting results below.
	// OnProgress blocks until the progress channel is closed (EventDone).
	done := make(chan struct{})
	go func() {
		defer close(done)
		unpixel.OnProgress(progCh, func(p unpixel.Progress) {
			if p.Kind == unpixel.EventNewBest {
				fmt.Println("new best:", p.BestGuess)
			}
		})
	}()

	for r := range resCh {
		if r.Err != nil {
			fmt.Println("result error:", r.Err)
			continue
		}
		fmt.Println("result:", r.BestGuess)
	}
	<-done
}

Index

Examples

Constants

View Source
const (
	// CharsetLower is lowercase ASCII letters plus space (the faithful default).
	CharsetLower = DefaultCharset
	// CharsetAlnum adds uppercase letters and digits to CharsetLower.
	CharsetAlnum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 "
	// CharsetASCII is every printable ASCII character (0x20–0x7E). Use it for
	// source code and other symbol-heavy text.
	CharsetASCII = " !\"#$%&'()*+,-./0123456789:;<=>?@" +
		"ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
)

Candidate-alphabet presets for Config.Charset (or WithCharset). A wider charset recovers more text but enlarges the search space, so prefer the narrowest one that fits the target.

View Source
const (
	// DefaultCharset is the candidate alphabet: lowercase ASCII letters and space.
	DefaultCharset = "abcdefghijklmnopqrstuvwxyz "
	// DefaultMaxLength is the maximum candidate string length the search explores.
	DefaultMaxLength = 20
	// DefaultBlockSize is the pixelation block side length in pixels.
	DefaultBlockSize = 8
	// DefaultThreshold is the per-block score gate for non-space characters;
	// candidates scoring above this value are pruned.
	DefaultThreshold = 0.25
	// DefaultSpaceThreshold is the per-block score gate for the space character,
	// which is visually ambiguous and requires a more permissive threshold.
	DefaultSpaceThreshold = 0.5
	// DefaultTopN is the default maximum number of ranked candidates retained
	// per grid offset and exposed on Result.TopN.
	DefaultTopN = 5
	// DefaultBeamWidth is the number of candidates retained per depth level by
	// BeamStrategy. Larger values improve recall at the cost of more evaluations.
	DefaultBeamWidth = 16
	// DefaultCacheSize is the maximum number of stageImage results held by
	// CachingScorer. Zero disables caching.
	DefaultCacheSize = 4096
)

Default configuration values, matching the original unredacter reference implementation.

Variables

View Source
var DefaultComponents func(cfg *Config) error

DefaultComponents is a hook that wires the standard Renderer, Pixelator, Metric, and Strategy into cfg for any fields that are still nil. It is populated by importing the defaults package for its side-effect, which avoids an import cycle between the root package and its internal implementations. Applications that supply all component fields explicitly need not set this.

View Source
var ErrNilImage = errors.New("redacted image must not be nil")

ErrNilImage is returned by New when a nil image is passed.

View Source
var ErrNoComponents = errors.New("unpixel: missing component and no DefaultComponents wired; " +
	"import github.com/oioio-space/unpixel/defaults or set the component via options")

ErrNoComponents is returned by the Recover helpers when the Config leaves a component (Renderer, Pixelator, Metric, or Strategy) nil and the defaults package has not been imported to wire them.

Functions

func InferBlockSize added in v0.2.0

func InferBlockSize(img image.Image) int

InferBlockSize estimates the pixelation block size, in pixels, of a mosaic- redacted image by detecting the spacing of its block grid. It scans for the columns and rows where the colour changes — the boundaries between constant- colour blocks — and returns the greatest common divisor of the gaps between them, which equals the block side length even when some adjacent blocks happen to share a colour (their gap is then a multiple of the block size).

It returns 0 when the grid cannot be determined — a uniform image, an image smaller than 2×2, or a non-pixelated image whose gaps reduce to 1. New uses it to fill a non-positive Config.BlockSize, falling back to DefaultBlockSize when inference returns 0.

func InferBlurSigma added in v0.4.0

func InferBlurSigma(img image.Image) float64

InferBlurSigma estimates the Gaussian-blur standard deviation (in pixels) of a blurred image, for zero-config blur recovery. A blurred high-contrast edge is a Gaussian-smoothed step, whose peak first derivative is A/(σ·√(2π)) for a step of amplitude A; solving for σ from the image's contrast A and its peak luminance gradient gives σ ≈ A / (gPeak·√(2π)).

It returns 0 when the image is too small or essentially flat. A returned value near 1 means the image is sharp (probably not blurred); a larger value is the estimated blur radius. The peak gradient uses a high percentile, not the max, to resist single-pixel noise.

func InferDarkBackground added in v0.3.0

func InferDarkBackground(img image.Image) bool

InferDarkBackground reports whether img looks like dark text/content on a dark background (e.g. a dark-mode screenshot), judged from its border pixels. New uses it to decide whether to invert the image so it matches the dark-on-light rendering pipeline.

func InferFontSize added in v0.4.0

func InferFontSize(img image.Image) float64

InferFontSize estimates the point size of the text in img from its content height, for zero-config calibration: rendered text spans about 0.92×fontSize vertically (ascenders to descenders), so fontSize ≈ contentHeight / 0.92. "Content" is any row that differs from the image's background colour.

It returns 0 when no content stands out. The estimate is a ballpark — a short word without ascenders/descenders, or blur spreading the glyphs, skews it — so it is best used to seed the search (optionally with a small size sweep) rather than as an exact value. Measure it on the redacted region (see LocateRedaction) or on a screenshot's sharp reference text, whichever is cleaner.

func LocateRedaction added in v0.4.0

func LocateRedaction(img image.Image) (image.Rectangle, bool)

LocateRedaction returns the bounding box of a blurred region inside img — for zero-config recovery of a redaction embedded in a larger screenshot. A blurred edge has a bounded peak gradient (≈ contrast/(σ·√(2π)), far below sharp text), so a row/column is "blurred content" when it carries contrast yet its peak luminance gradient stays low. The result is the tight box of the largest such band; ok is false when no blurred region stands out (e.g. an all-sharp image).

It is the missing piece for screenshots: estimating sigma or recovering on the whole image is skewed by sharp surrounding text (on the Bishop Fox challenge, whole-image σ≈0.6 vs ≈5.6 on the blurred line), so crop to this box first.

func OnProgress

func OnProgress(ch <-chan Progress, fn func(Progress))

OnProgress drains the progress channel returned by Engine.Run, calling fn for every event in order. It blocks until the channel is closed (i.e. until EventDone has been delivered and consumed). It is a convenience wrapper around a range loop and does not start a goroutine.

Types

type Config

type Config struct {
	// Renderer is the component that rasterises candidate strings to RGBA.
	// Defaults to the XImage renderer (wired by the defaults package).
	Renderer Renderer
	// Pixelator is the component that applies block-average pixelation.
	// Defaults to BlockAverage (wired by the defaults package).
	Pixelator Pixelator
	// Metric is the component that measures pixel-level image distance.
	// Defaults to Pixelmatch (wired by the defaults package).
	Metric Metric
	// Strategy is the component that drives the search algorithm.
	// Defaults to GuidedDFS (wired by the defaults package).
	Strategy Strategy

	// LanguageModel optionally scores a candidate string's linguistic
	// plausibility (higher = more plausible). When set, the final ranking breaks
	// ties between candidates of equal image distance toward plausible text — the
	// image cannot separate visually near-identical candidates (especially behind
	// heavy blur), but a language prior can. nil disables it (the default).
	LanguageModel func(string) float64

	// ThresholdFor returns the acceptance threshold for a given candidate
	// character. It defaults to a closure that returns SpaceThreshold for ' '
	// and Threshold for all other runes. Override to apply per-class thresholds
	// without modifying search logic.
	ThresholdFor func(rune) float64

	// Charset is the ordered set of candidate characters tried at each search
	// depth. Defaults to DefaultCharset (a–z plus space).
	Charset string

	// Style controls the font size, weight, and padding used when rendering
	// candidates. Zero fields use the design defaults (32 pt, 8 px padding).
	Style Style

	// MaxLength is the maximum number of characters the search will attempt
	// before backtracking. Defaults to DefaultMaxLength.
	MaxLength int
	// BlockSize is the side length in pixels of each pixelation block. It must
	// match the block size used to produce the redacted image. When left
	// non-positive, New auto-detects it from the image via InferBlockSize,
	// falling back to DefaultBlockSize if the grid cannot be determined.
	BlockSize int
	// Threshold is the per-block score below which a non-space candidate is
	// accepted and the search descends deeper. Lower values are stricter.
	// Defaults to DefaultThreshold.
	Threshold float64
	// SpaceThreshold is the acceptance threshold applied specifically to the
	// space character, which is harder to distinguish visually. Defaults to
	// DefaultSpaceThreshold.
	SpaceThreshold float64
	// TopN is the maximum number of ranked candidates retained per grid offset
	// in Result.TopN. Candidates are sorted ascending by score (lowest first);
	// ties are broken by Guess string so results are deterministic. Zero or
	// negative values are replaced by DefaultTopN in applyDefaults.
	TopN int

	// BeamWidth is the maximum number of candidates retained per depth level
	// when using BeamStrategy. Values <= 0 are replaced by DefaultBeamWidth.
	// Has no effect when Strategy is GuidedStrategy.
	BeamWidth int
	// CacheSize is the maximum number of stageImage results held by
	// CachingScorer. Zero disables prefix-render memoization; positive values
	// enable an LRU cache of that capacity shared across all Eval calls for a
	// single Search invocation. Defaults to DefaultCacheSize.
	CacheSize int
	// Workers is the maximum number of grid offsets probed and searched
	// concurrently. Values <= 0 default to runtime.GOMAXPROCS. Set to 1 to force
	// fully sequential execution. Results are merged deterministically regardless
	// of Workers, so this affects throughput only, never the output.
	Workers int
}

Config holds all tunable parameters for an Engine. Every field is optional: zero values are replaced by the package defaults (see the Default* constants) when New is called. Component fields (Renderer, Pixelator, Metric, Strategy) are filled by importing the defaults package for its side-effect, or can be supplied directly for custom implementations.

type Engine

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

Engine orchestrates the full unpixel pipeline: grid-origin discovery, candidate rendering, re-pixelation, distance measurement, and guided search. Create one with New and call Run to start the search.

func New

func New(redacted image.Image, cfg Config) (*Engine, error)

New creates an Engine for the given pixelated image. Scalar zero values in cfg are replaced by package defaults; nil component fields are left for Run to fill via DefaultComponents (or must be set explicitly). New returns ErrNilImage if redacted is nil.

func (*Engine) Config added in v0.2.0

func (e *Engine) Config() Config

Config returns the engine's resolved configuration, with scalar defaults applied and BlockSize auto-inferred. Component fields (Renderer, Pixelator, Metric, Strategy) are non-nil only after Run has wired them. It is intended for introspection — for example, reading the inferred BlockSize.

func (*Engine) Run

func (e *Engine) Run(ctx context.Context) (<-chan Progress, <-chan Result)

Run starts the search and returns a progress channel and a results channel. The progress channel carries Progress events and is closed after EventDone is delivered. The results channel receives one Result per surviving grid offset and is closed once all offsets have been searched. Cancelling ctx causes both channels to be closed promptly after the in-flight search step completes.

If any component field in cfg is nil, Run invokes DefaultComponents to fill it before starting. If DefaultComponents is also nil, Run panics with a descriptive message directing the caller to import the defaults package.

type Eval

type Eval struct {
	// GuessImage is a cropped, pixelated rendering of Guess, populated only
	// when the caller opts in via the Strategy implementation.
	GuessImage *image.RGBA
	// Guess is the candidate string evaluated at this node.
	Guess string
	// Score is the marginal region diff score for the most recently added
	// character (lower is better; compared against the threshold).
	Score float64
	// TooBig is true when the rendered candidate is wider than the redacted
	// image; such candidates are pruned regardless of score.
	TooBig bool
}

Eval holds the result of evaluating a single candidate string at one search node.

func (Eval) String added in v0.3.0

func (e Eval) String() string

String returns the candidate and its marginal score, e.g. `"hello" (0.0123)`.

type EventKind

type EventKind int

EventKind identifies the type of a Progress event delivered on the channel returned by Engine.Run.

const (
	// EventCandidate is emitted for every candidate string evaluated during
	// the search. It is high-frequency; the channel is non-blocking and events
	// are dropped when the buffer is full.
	EventCandidate EventKind = iota
	// EventOffsetProbed is emitted after each grid-origin probe during the
	// offset-discovery phase. It is high-frequency with the same drop semantics
	// as EventCandidate.
	EventOffsetProbed
	// EventNewBest is emitted whenever the search finds a new overall best
	// guess. Delivery is guaranteed: the sender blocks (with context) until the
	// consumer reads the event.
	EventNewBest
	// EventDone is the terminal event, delivered exactly once after all offsets
	// have been searched or the context is cancelled. Delivery is guaranteed.
	EventDone
)

type FontResult added in v0.3.0

type FontResult struct {
	// Result is the recovery produced with this renderer.
	Result Result
	// Index is the position of the renderer in the slice passed to
	// RecoverMultiFont, identifying which font produced this Result.
	Index int
}

FontResult pairs one candidate renderer with the Result it produced, as returned by RecoverMultiFont.

func RecoverMultiFont added in v0.3.0

func RecoverMultiFont(ctx context.Context, redacted image.Image, renderers []Renderer, opts ...Option) ([]FontResult, error)

RecoverMultiFont runs Recover once per renderer and returns the results ranked best-fit first, so the caller can recover a redaction without knowing its exact typeface — supply several candidate fonts and let the tool pick.

Ranking is by Result.BestTotal, the whole-image distance of the best guess: unlike BestScore (a per-character marginal score that ties at ~0 across fonts), BestTotal is comparable between separate runs, so the lowest-BestTotal font is the one that reconstructs the redaction most faithfully. ret[0] is the best fit. A renderer with an empty recovery is ranked last; a renderer whose run errors is omitted. RecoverMultiFont returns an error only when redacted is nil, renderers is empty, or every renderer failed.

The runs execute in parallel within a core budget — the resolved Config.Workers (set via WithWorkers or WithConfig), or runtime.GOMAXPROCS when unset: min(len(renderers), budget) run concurrently, each search using an equal share of workers, so the two layers never oversubscribe the CPU. The per-run renderer and worker count are set internally, overriding any WithRenderer/WithWorkers already in opts.

Build the renderers with github.com/oioio-space/unpixel/defaults.RendererFromFonts:

var rs []unpixel.Renderer
for _, data := range fontData {
    r, err := defaults.RendererFromFonts(data, nil)
    if err != nil { return err }
    rs = append(rs, r)
}
ranked, err := unpixel.RecoverMultiFont(ctx, img, rs, unpixel.WithBlockSize(5))
best := ranked[0]

type Metric

type Metric interface {
	Compare(a, b *image.RGBA) float64
}

Metric measures the visual distance between two same-sized RGBA images. Compare returns a value in [0, 1] where 0 means pixel-perfect identical.

type Offset

type Offset struct {
	// X is the horizontal grid origin in pixels.
	X int
	// Y is the vertical grid origin in pixels.
	Y int
	// Score is the image-distance score for this origin (lower is better).
	Score float64
}

Offset represents one candidate grid origin for the pixelation block alignment. The search probes multiple (X, Y) origins and ranks them by Score before committing to the full character-level search.

type Option added in v0.3.0

type Option func(*Config)

Option configures a Config for the high-level Recover helpers. Options are applied in order, so a later option overrides an earlier one; WithConfig (which replaces the whole Config) is therefore best passed first.

func WithBlockSize added in v0.3.0

func WithBlockSize(n int) Option

WithBlockSize sets Config.BlockSize. A non-positive value lets New auto-detect the block size from the image (see InferBlockSize).

func WithCharset added in v0.3.0

func WithCharset(charset string) Option

WithCharset sets Config.Charset (the ordered candidate alphabet).

func WithConfig added in v0.3.0

func WithConfig(cfg Config) Option

WithConfig uses cfg as the starting point before any other options are applied. Pass it first to layer further options on top of a full Config.

func WithLanguageModel added in v0.4.0

func WithLanguageModel(score func(string) float64) Option

WithLanguageModel sets Config.LanguageModel, a plausibility scorer used to break ties between candidates of equal image distance toward plausible text.

func WithMaxLength added in v0.3.0

func WithMaxLength(n int) Option

WithMaxLength sets Config.MaxLength (the maximum candidate length).

func WithMetric added in v0.3.0

func WithMetric(m Metric) Option

WithMetric sets Config.Metric (the image-distance metric).

func WithPixelator added in v0.4.0

func WithPixelator(p Pixelator) Option

WithPixelator sets Config.Pixelator (the redaction operator the search reproduces). Use it with defaults.GaussianBlur(sigma) and WithBlockSize(1) to recover blurred text instead of mosaic pixelation.

func WithRenderer added in v0.3.0

func WithRenderer(r Renderer) Option

WithRenderer sets Config.Renderer (the text rasteriser). Use it with a custom font, e.g. defaults.RendererFromFonts, to match the typeface of the redacted image.

func WithStrategy added in v0.3.0

func WithStrategy(s Strategy) Option

WithStrategy sets Config.Strategy (the search algorithm).

func WithStyle added in v0.3.0

func WithStyle(s Style) Option

WithStyle sets Config.Style (font size, weight, and padding). It must match the style of the redacted text for recovery to succeed.

func WithThreshold added in v0.3.0

func WithThreshold(t float64) Option

WithThreshold sets Config.Threshold (the per-block acceptance gate).

func WithTopN added in v0.3.0

func WithTopN(n int) Option

WithTopN sets Config.TopN (ranked candidates retained per offset).

func WithWorkers added in v0.3.0

func WithWorkers(n int) Option

WithWorkers sets Config.Workers (max grid offsets searched concurrently; non-positive means runtime.GOMAXPROCS).

type Pixelator

type Pixelator interface {
	Pixelate(img *image.RGBA, originX, originY int) *image.RGBA
}

Pixelator replaces every blockSize×blockSize region of an image with the mean RGBA colour of that region. The originX and originY parameters align the grid so that it matches the original pixelation applied to the source.

type Progress

type Progress struct {
	// Err carries any terminal error that caused the search to abort.
	Err error

	// PreviewImage is an optional deep copy of the pixelated guess image.
	// It is non-nil only when the Strategy implementation populates it.
	PreviewImage *image.RGBA

	// BestGuess is the candidate string with the lowest score found so far
	// across all offsets.
	BestGuess string

	// Guess is the candidate string that triggered this specific event.
	Guess string
	// Offset is the grid origin currently being searched.
	Offset Offset
	// Kind identifies which event this Progress value represents.
	Kind EventKind

	// BestScore is the total image-distance score of BestGuess (lower is better).
	BestScore float64

	// Score is the image-distance score of Guess.
	Score float64

	// Depth is the current search depth, equal to the character length of Guess.
	Depth int
	// Evaluated is the cumulative number of candidates evaluated since Run started.
	Evaluated int
	// OffsetsDone is the number of grid offsets fully searched so far.
	OffsetsDone int
	// OffsetsTotal is the total number of surviving grid offsets to be searched.
	OffsetsTotal int

	// Elapsed is the wall-clock duration since Engine.Run was called.
	Elapsed time.Duration
	// Done is true only on the EventDone event.
	Done bool
}

Progress carries a snapshot of search state, streamed on the channel returned by Engine.Run. Consumers should switch on Kind to decide which fields are meaningful for a given event.

type Renderer

type Renderer interface {
	Render(text string, style Style) (img *image.RGBA, sentinelX int, err error)
}

Renderer renders candidate text to an RGBA image, placing a blue sentinel block immediately to the right of the text. It returns the image and the x-coordinate of that sentinel, which marks the text's right edge in pixels.

type Result

type Result struct {
	// Err is non-nil if the search for this offset aborted due to an error.
	Err error
	// BestGuess is the candidate string with the lowest overall score found
	// during the search rooted at this offset.
	BestGuess string
	// Candidates holds every string that passed the threshold gate during the
	// search, in discovery order.
	Candidates []Eval
	// TopN holds the best candidates sorted ascending by score (lowest score
	// first), with ties broken by Guess string for determinism. Its length is
	// at most Config.TopN. TopN[0] is the same candidate as BestGuess when the
	// search produces any result. TopN is nil when no candidates were found.
	TopN []Eval
	// Offset is the grid origin that was searched to produce this result.
	Offset Offset
	// BestScore is the marginal-region diff score of BestGuess (lower is better).
	BestScore float64
	// BestTotal is the whole-image distance of BestGuess against the redaction
	// (0 = pixel-perfect, 1 = worst). Unlike BestScore — which is a per-character
	// marginal score that a correct prefix or a coincidental match can drive to
	// ~0 — BestTotal measures how well the full guess explains the entire
	// redaction, so it is comparable across separate runs. That makes it the
	// right signal for choosing between candidate fonts or styles: the run with
	// the lowest BestTotal recovered the redaction most faithfully.
	BestTotal float64
	// Confidence is 1 − TopN[0].Score, giving a value in [0, 1] where 1
	// represents a pixel-perfect match. It is 0 when TopN is empty.
	Confidence float64
	// Ambiguity is TopN[1].Score − TopN[0].Score: the score gap between the
	// best and second-best candidates. A larger gap means the best guess is
	// more clearly distinguished from alternatives. It is 0 when len(TopN) < 2.
	Ambiguity float64
}

Result is the final output produced by Engine.Run for one surviving grid offset. One Result is sent per offset on the results channel returned by Run.

func Recover added in v0.3.0

func Recover(ctx context.Context, redacted image.Image, opts ...Option) (Result, error)

Recover is the one-call entry point: it runs the full search on the redacted image and returns the single best Result across all grid offsets. It builds a Config from opts, auto-detects what it can (e.g. the block size), wires the default components (when the defaults package is imported), runs the search, and drains the channels for the caller.

The common usage is a side-effect import of the defaults package:

import _ "github.com/oioio-space/unpixel/defaults"
res, err := unpixel.Recover(ctx, img)
fmt.Println(res.BestGuess)

Recover returns ErrNilImage for a nil image and ErrNoComponents when a component is missing and no defaults are wired. A search that finds nothing returns the zero Result and a nil error.

func RecoverFile added in v0.3.0

func RecoverFile(ctx context.Context, path string, opts ...Option) (Result, error)

RecoverFile opens the image at path and calls RecoverReader. Pass "-" handling at the call site if stdin support is needed; RecoverFile always opens a file.

func RecoverReader added in v0.3.0

func RecoverReader(ctx context.Context, r io.Reader, opts ...Option) (Result, error)

RecoverReader decodes an image (PNG is registered; register other formats by importing their image/<fmt> package) from r and calls Recover.

func (Result) String added in v0.3.0

func (r Result) String() string

String returns a one-line human-readable summary of the result: the best guess with its score and confidence, or an error / "no result" note.

type Strategy

type Strategy interface {
	Search(ctx context.Context, redacted *image.RGBA, cfg Config, out chan<- Progress, results chan<- Result)
}

Strategy runs the full character-by-character search over the candidate space and streams progress and results on the provided channels. Implementations may use different algorithms (guided DFS, beam search, etc.). Search must close neither channel; Engine.Run owns their lifecycle.

type Style

type Style struct {
	// FontSize is the font size in points. Defaults to 32.
	FontSize float64
	// Bold selects the bold font variant when true.
	Bold bool
	// PaddingTop is the top padding applied before the text, in pixels. Defaults to 8.
	PaddingTop int
	// PaddingLeft is the left padding applied before the text, in pixels. Defaults to 8.
	PaddingLeft int
	// LetterSpacing is extra horizontal space, in pixels, inserted after each
	// glyph — the equivalent of the CSS letter-spacing property. It may be
	// negative to tighten text (e.g. -0.2 to match a Consolas redaction). The
	// default 0 leaves glyph advances untouched and preserves kerning; a
	// non-zero value renders glyph-by-glyph without kerning.
	LetterSpacing float64
}

Style describes the text rendering parameters that must match the typography of the original, pre-pixelated screenshot.

Directories

Path Synopsis
cmd
unpixel command
Command unpixel reconstructs text hidden behind pixelation.
Command unpixel reconstructs text hidden behind pixelation.
Package defaults wires the standard internal components into an unpixel.Config.
Package defaults wires the standard internal components into an unpixel.Config.
Package fonts bundles a small set of redistributable typefaces so UnPixel can recover a redaction zero-config — without the caller supplying any font file — by sweeping these candidates and keeping the best fit (see github.com/oioio-space/unpixel.RecoverMultiFont).
Package fonts bundles a small set of redistributable typefaces so UnPixel can recover a redaction zero-config — without the caller supplying any font file — by sweeping these candidates and keeping the best fit (see github.com/oioio-space/unpixel.RecoverMultiFont).
internal
fixture
Package fixture generates synthetic pixelated-text reference images for the recovery test matrix.
Package fixture generates synthetic pixelated-text reference images for the recovery test matrix.
fixture/gen command
Command gen renders the fixture.Matrix reference images to an output directory, alongside a manifest.json that records each image's filename and its original generation parameters (the image ↔ parameters link).
Command gen renders the fixture.Matrix reference images to an output directory, alongside a manifest.json that records each image's filename and its original generation parameters (the image ↔ parameters link).
imutil
Package imutil provides image manipulation helpers used by the unpixel pipeline.
Package imutil provides image manipulation helpers used by the unpixel pipeline.
lang
Package lang provides a tiny, pure-Go character bigram language model used as a prior over candidate strings.
Package lang provides a tiny, pure-Go character bigram language model used as a prior over candidate strings.
metric
Package metric provides image distance metrics for the unpixel pipeline.
Package metric provides image distance metrics for the unpixel pipeline.
pixelate
Package pixelate (blur.go) adds a Gaussian-blur redaction operator alongside the block-average pixelator.
Package pixelate (blur.go) adds a Gaussian-blur redaction operator alongside the block-average pixelator.
render
Package render provides the XImage text renderer for the unpixel pipeline.
Package render provides the XImage text renderer for the unpixel pipeline.
search
Package search (beam.go) adds BeamDFS, a beam-search variant of GuidedDFS that bounds the branching factor by retaining only the best cfg.BeamWidth candidates per depth level, and BeamStrategy, the unpixel.Strategy that drives it.
Package search (beam.go) adds BeamDFS, a beam-search variant of GuidedDFS that bounds the branching factor by retaining only the best cfg.BeamWidth candidates per depth level, and BeamStrategy, the unpixel.Strategy that drives it.

Jump to

Keyboard shortcuts

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