diff --git a/docs/changelog.d/+qart-tuner.feature.md b/docs/changelog.d/+qart-tuner.feature.md new file mode 100644 index 0000000..720774d --- /dev/null +++ b/docs/changelog.d/+qart-tuner.feature.md @@ -0,0 +1 @@ +Add QArt Tuner — a Go tool that generates QR codes whose data modules form a recognizable image, with an interactive web UI for parameter tuning. Based on the [QArt technique](https://research.swtch.com/qart) by Russ Cox. Lives in `utils/qart/`. diff --git a/docs/reference/tools/mise-tasks.md b/docs/reference/tools/mise-tasks.md index d2385a6..ae92013 100644 --- a/docs/reference/tools/mise-tasks.md +++ b/docs/reference/tools/mise-tasks.md @@ -89,3 +89,4 @@ Run `mise tasks --sort name` for the live list with descriptions. - [[ansible]] — Configuration management - [[argocd-cli]] — ArgoCD deployment workflows - [[pulumi]] — DNS and Tailscale IaC +- [[qart-tuner]] — QR code art generator (`utils/qart/`) diff --git a/docs/reference/tools/qart-tuner.md b/docs/reference/tools/qart-tuner.md new file mode 100644 index 0000000..a05c302 --- /dev/null +++ b/docs/reference/tools/qart-tuner.md @@ -0,0 +1,61 @@ +--- +title: QArt Tuner +modified: 2026-03-27 +tags: + - reference + - tools + - utils +--- + +# QArt Tuner + +Generates QR codes whose data modules form a recognizable image, using the [QArt technique](https://research.swtch.com/qart) by Russ Cox. Lives in `utils/qart/`. + +## Quick Reference + +| Item | Value | +|------|-------| +| **Source** | `utils/qart/main.go` | +| **Language** | Go (managed via mise) | +| **Dependency** | [rsc.io/qr](https://github.com/rsc/qr) (BSD 3-clause) | +| **Launch web UI** | `QART_IMAGE=photo.png mise run serve` (from `utils/qart/`) | +| **CLI** | `mise x go -- go run . -url URL -image IMG -out out.png` | + +## How It Works + +QR error correction (Reed-Solomon coding) allows some data and check bits to be freely chosen. The tool: + +1. Builds a QR code plan for the given URL and version +2. Converts the source photo to a grayscale brightness grid at QR module resolution +3. For each ECC block, models the data/check bit relationships as a matrix over GF(2) +4. Uses Gaussian elimination to find which bits can be independently assigned +5. Assigns bits to match the target image brightness, prioritizing high-contrast areas + +The result is a valid, scannable QR code whose black/white modules approximate the source image. + +## Web UI + +The interactive tuner (`-serve` flag) provides sliders for all parameters with live preview. + +**Keyboard shortcuts:** arrow keys (dx/dy offset), `[`/`]` (mask), `-`/`=` (version), `r` (rotate). + +## Parameters + +| Parameter | Range | Effect | +|-----------|-------|--------| +| **version** | 1-8 | QR density — higher = more modules = finer detail | +| **mask** | 0-7 | QR mask pattern — dramatically affects which pixels are controllable | +| **dx/dy** | -15 to 15 | Shifts image relative to QR structure (avoids alignment dot on eyes) | +| **rotation** | 0-3 | Quarter turns | +| **scale** | 1-16 | Output pixels per QR module | +| **dither** | on/off | Floyd-Steinberg dithering | + +## Credits + +- **Technique:** [Russ Cox](https://swtch.com/~rsc/), [QArt Codes](https://research.swtch.com/qart) (2012) +- **QR library:** [rsc.io/qr](https://github.com/rsc/qr) — QR layout, encoding, GF(256) arithmetic +- **Implementation:** Claude Code (Opus 4.6) with direction from Erich Blume + +## Related + +- [[mise-tasks]] — Task runner for BlumeOps operations diff --git a/utils/qart/.gitignore b/utils/qart/.gitignore new file mode 100644 index 0000000..7b4f4ae --- /dev/null +++ b/utils/qart/.gitignore @@ -0,0 +1,3 @@ +qart +qart-gen +*.png diff --git a/utils/qart/README.md b/utils/qart/README.md new file mode 100644 index 0000000..459f65e --- /dev/null +++ b/utils/qart/README.md @@ -0,0 +1,83 @@ +# QArt Tuner + +Generate QR codes whose data modules form a recognizable image. + +This implements the [QArt technique](https://research.swtch.com/qart) invented +by [Russ Cox](https://swtch.com/~rsc/). The trick: QR error correction gives +some freedom in choosing bit values. By picking bits that satisfy the +Reed-Solomon constraints *and* match a target image's brightness, the QR +modules themselves draw a picture — no logo overlay, no center cutout. + +This tool uses the [rsc.io/qr](https://github.com/rsc/qr) library (BSD +3-clause) for QR layout, data encoding, and GF(256) arithmetic. The +image-targeting algorithm — contrast-priority bit selection via GF(2) Gaussian +elimination — is an original implementation based on the technique description +in Russ Cox's blog post. + +## Quick start + +```fish +# Launch the interactive web UI +QART_IMAGE=~/path/to/photo.png mise run serve + +# Or with a custom URL and port +QART_URL=https://example.com QART_IMAGE=photo.png QART_PORT=9090 mise run serve +``` + +The web UI lets you adjust version, mask, rotation, x/y offset, and scale with +live preview. Keyboard shortcuts: arrow keys (dx/dy), `[`/`]` (mask), `-`/`=` +(version), `r` (rotate). + +## CLI usage + +```fish +# Single image +mise x go -- go run . -url "https://docs.eblu.me" -image photo.png -out qart.png \ + -version 6 -mask 4 -dx 6 -dy 4 -scale 8 + +# All 8 mask variants +mise x go -- go run . -url "https://docs.eblu.me" -image photo.png -out qart.png -all-masks +``` + +### Flags + +| Flag | Default | Description | +|------|---------|-------------| +| `-url` | (required) | URL to encode in the QR code | +| `-image` | (required) | Source photo (PNG or JPEG) | +| `-out` | `qart.png` | Output file path | +| `-version` | `6` | QR version (1-8, higher = more modules = more detail) | +| `-mask` | `0` | QR mask pattern (0-7, affects visual texture) | +| `-scale` | `8` | Pixels per QR module | +| `-rotation` | `0` | Quarter turns (0-3) | +| `-dx` | `0` | Horizontal image offset (-15 to 15) | +| `-dy` | `0` | Vertical image offset (-15 to 15) | +| `-dither` | `false` | Enable Floyd-Steinberg dithering | +| `-seed` | (random) | RNG seed for reproducible output | +| `-all-masks` | `false` | Generate all 8 mask variants | +| `-serve` | `false` | Launch web UI instead of writing a file | +| `-port` | `8088` | Web UI port | + +## Tips + +- **Version** controls QR density. Higher = more modules = finer image detail, + but the code becomes harder to scan at small sizes. +- **Mask** dramatically affects which pixels the algorithm can control. Try all + 8 — the best one varies per image. +- **dx/dy offsets** shift the image relative to the QR structure. Use this to + avoid the central alignment dot landing on an eye (it makes you look + unhinged). +- The QR code uses **error correction level L** (lowest) to maximize the number + of bits available for image rendering. + +## Credits + +The QArt technique was invented by Russ Cox and described in his 2012 blog +post [QArt Codes](https://research.swtch.com/qart). The +[rsc.io/qr](https://github.com/rsc/qr) library provides the QR code +primitives this tool builds on. Thank you, Russ. + +This tool was written by [Claude Code](https://claude.ai/claude-code) (Opus +4.6) with direction from Erich Blume. The image-targeting algorithm is an +original implementation based on the technique description — not a copy of +rsc's reference implementation. diff --git a/utils/qart/go.mod b/utils/qart/go.mod new file mode 100644 index 0000000..51c6481 --- /dev/null +++ b/utils/qart/go.mod @@ -0,0 +1,5 @@ +module qart + +go 1.25.7 + +require rsc.io/qr v0.2.0 // indirect diff --git a/utils/qart/go.sum b/utils/qart/go.sum new file mode 100644 index 0000000..19a61d9 --- /dev/null +++ b/utils/qart/go.sum @@ -0,0 +1,2 @@ +rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= +rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/utils/qart/main.go b/utils/qart/main.go new file mode 100644 index 0000000..103fe72 --- /dev/null +++ b/utils/qart/main.go @@ -0,0 +1,753 @@ +// QArt Tuner — generates QR codes whose data modules form a recognizable image. +// +// This tool implements the QArt technique described by Russ Cox at +// https://research.swtch.com/qart. The technique exploits QR error correction: +// by choosing data/check bit values that satisfy the Reed-Solomon constraints +// while also matching a target image's brightness, the QR modules themselves +// draw a picture. +// +// This implementation uses the rsc.io/qr library for QR layout, encoding, and +// GF(256) arithmetic. The image-targeting algorithm (bit selection via GF(2) +// Gaussian elimination, contrast-priority ordering, and image preprocessing) +// is an original implementation based on the technique description. +// +// Written by Claude Code (Opus 4.6) with direction from Erich Blume. +package main + +import ( + "bytes" + "flag" + "fmt" + "image" + _ "image/jpeg" + _ "image/png" + "math/rand" + "net/http" + "os" + "os/exec" + "runtime" + "sort" + "strconv" + "time" + + "rsc.io/qr" + "rsc.io/qr/coding" + "rsc.io/qr/gf256" +) + +// --------------------------------------------------------------------------- +// CLI & web server +// --------------------------------------------------------------------------- + +func main() { + var ( + url = flag.String("url", "", "URL to encode") + imgPath = flag.String("image", "", "path to source image") + outPath = flag.String("out", "qart.png", "output PNG path") + version = flag.Int("version", 6, "QR version (1-8)") + mask = flag.Int("mask", 0, "QR mask pattern (0-7)") + scale = flag.Int("scale", 8, "pixel scale factor") + dither = flag.Bool("dither", false, "use dithering") + rotation = flag.Int("rotation", 0, "rotation (0-3, quarter turns)") + dx = flag.Int("dx", 0, "image X offset (positive = shift right)") + dy = flag.Int("dy", 0, "image Y offset (positive = shift down)") + seed = flag.Int64("seed", 0, "random seed (0 = use time)") + allMasks = flag.Bool("all-masks", false, "generate all 8 mask variants") + serve = flag.Bool("serve", false, "start web UI for interactive tuning") + port = flag.Int("port", 8088, "port for web UI") + ) + flag.Parse() + + if *url == "" || *imgPath == "" { + fmt.Fprintf(os.Stderr, "usage: qart-gen -url URL -image IMAGE [-out OUTPUT] [-serve]\n") + os.Exit(1) + } + + imgData, err := os.ReadFile(*imgPath) + if err != nil { + fmt.Fprintf(os.Stderr, "reading image: %v\n", err) + os.Exit(1) + } + + if *serve { + startServer(imgData, *url, *port) + return + } + + pickSeed := func() int64 { + if *seed != 0 { + return *seed + } + return time.Now().UnixNano() + } + + if *allMasks { + for m := 0; m < 8; m++ { + out := fmt.Sprintf("%s_mask%d.png", (*outPath)[:len(*outPath)-4], m) + pngBytes, err := renderQArt(imgData, *url, *version, m, *scale, *rotation, *dx, *dy, *dither, pickSeed()) + if err != nil { + fmt.Fprintf(os.Stderr, "mask %d: %v\n", m, err) + continue + } + os.WriteFile(out, pngBytes, 0644) + fmt.Printf("wrote %s\n", out) + } + } else { + pngBytes, err := renderQArt(imgData, *url, *version, *mask, *scale, *rotation, *dx, *dy, *dither, pickSeed()) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + os.WriteFile(*outPath, pngBytes, 0644) + fmt.Printf("wrote %s\n", *outPath) + } +} + +func intParam(r *http.Request, name string, def int) int { + v := r.URL.Query().Get(name) + if v == "" { + return def + } + n, err := strconv.Atoi(v) + if err != nil { + return def + } + return n +} + +func startServer(imgData []byte, url string, port int) { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(indexHTML)) + }) + + renderHandler := func(w http.ResponseWriter, r *http.Request, download bool) { + version := intParam(r, "version", 6) + mask := intParam(r, "mask", 0) + scale := intParam(r, "scale", 8) + rotation := intParam(r, "rotation", 0) + dx := intParam(r, "dx", 0) + dy := intParam(r, "dy", 0) + dither := r.URL.Query().Get("dither") == "1" + seed := int64(intParam(r, "seed", 0)) + if seed == 0 { + seed = time.Now().UnixNano() + } + + pngData, err := renderQArt(imgData, url, version, mask, scale, rotation, dx, dy, dither, seed) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + w.Header().Set("Content-Type", "image/png") + if download { + w.Header().Set("Content-Disposition", + fmt.Sprintf("attachment; filename=qart_v%d_m%d_dx%d_dy%d.png", version, mask, dx, dy)) + } else { + w.Header().Set("Cache-Control", "no-cache") + } + w.Write(pngData) + } + + http.HandleFunc("/generate", func(w http.ResponseWriter, r *http.Request) { + renderHandler(w, r, false) + }) + http.HandleFunc("/save", func(w http.ResponseWriter, r *http.Request) { + renderHandler(w, r, true) + }) + + addr := fmt.Sprintf("localhost:%d", port) + fmt.Printf("QArt tuner running at http://%s\n", addr) + if runtime.GOOS == "darwin" { + exec.Command("open", "http://"+addr).Start() + } + if err := http.ListenAndServe(addr, nil); err != nil { + fmt.Fprintf(os.Stderr, "server error: %v\n", err) + os.Exit(1) + } +} + +// --------------------------------------------------------------------------- +// QArt rendering — original implementation of the technique from +// https://research.swtch.com/qart using only the public rsc.io/qr API. +// --------------------------------------------------------------------------- + +// renderQArt produces a PNG of a QR code encoding url whose data modules +// approximate the brightness pattern of the source image. +func renderQArt(imgData []byte, url string, version, mask, scale, rotation, dx, dy int, dither bool, seed int64) ([]byte, error) { + if version > 8 { + version = 8 + } + if scale < 1 { + scale = 8 + } + + // Build the QR plan — this tells us where every pixel goes and its role. + plan, err := coding.NewPlan(coding.Version(version), coding.L, coding.Mask(mask)) + if err != nil { + return nil, err + } + + rotatePixels(plan, rotation) + + // Build the grayscale target image scaled to QR module grid size. + gridSize := 17 + 4*version + target, err := imageToTarget(imgData, gridSize) + if err != nil { + return nil, err + } + + // Encode the URL into QR data, filling remaining capacity with numeric + // padding that we can freely manipulate. + urlStr := url + "#" + var bits coding.Bits + coding.String(urlStr).Encode(&bits, plan.Version) + coding.Num("").Encode(&bits, plan.Version) + fixedBits := bits.Bits() + freeBits := plan.DataBytes*8 - fixedBits + if freeBits < 0 { + return nil, fmt.Errorf("URL too long for QR version %d", version) + } + + // Fill free space with numeric '0's — 10 bits per 3 digits. + numDigits := freeBits / 10 * 3 + num := make([]byte, numDigits) + for i := range num { + num[i] = '0' + } + + rng := rand.New(rand.NewSource(seed)) + + // Iterate: encode, manipulate bits to match image, extract numeric + // digits back out, re-encode. Loop if any 10-bit group >= 1000. + type pixInfo struct { + x, y int + pix coding.Pixel + targ byte // target brightness (0=black, 255=white) + contrast int // local contrast (variance); higher = more visually important + hardZero bool + } + + var hardZeros map[int]bool + + for attempt := 0; attempt < 10; attempt++ { + bits.Pad(freeBits) + bits.Reset() + coding.String(urlStr).Encode(&bits, plan.Version) + coding.Num(coding.Num(num)).Encode(&bits, plan.Version) + bits.AddCheckBytes(plan.Version, plan.Level) + data := bits.Bytes() + + // Index every data/check pixel by its bit offset in the codeword stream. + totalBits := (plan.DataBytes + plan.CheckBytes) * 8 + pixByOff := make([]pixInfo, totalBits) + for y, row := range plan.Pixel { + for x, pix := range row { + role := pix.Role() + if role != coding.Data && role != coding.Check { + continue + } + t, c := targetAt(target, x+dx, y+dy) + if c >= 0 { + c = c<<8 | rng.Intn(256) + } + pi := pixInfo{x: x, y: y, pix: pix, targ: t, contrast: c} + if hardZeros[int(pix.Offset())] { + pi.hardZero = true + pi.contrast = 1<<30 | rng.Intn(256) + } + pixByOff[pix.Offset()] = pi + } + } + + // Process each ECC block independently. + ndBase := plan.DataBytes / plan.Blocks + nc := plan.CheckBytes / plan.Blocks + extra := plan.DataBytes - ndBase*plan.Blocks + rs := gf256.NewRSEncoder(coding.Field, nc) + usableBits := fixedBits + freeBits/10*10 + + doff, coff := 0, 0 + for block := 0; block < plan.Blocks; block++ { + nd := ndBase + if block >= plan.Blocks-extra { + nd++ + } + bdata := data[doff/8 : doff/8+nd] + cdata := data[plan.DataBytes+coff/8 : plan.DataBytes+coff/8+nc] + + bb := newBitBlock(nd, nc, rs, bdata, cdata) + + // Determine the editable bit range within this block's data bytes. + lo, hi := 0, nd*8 + if fixedBits-doff > lo { + lo = fixedBits - doff + } + if lo > hi { + lo = hi + } + if usableBits-doff < hi { + hi = usableBits - doff + } + if hi < lo { + hi = lo + } + + // Lock the preserved bits. + for i := 0; i < lo; i++ { + bb.fix(uint(i), (bdata[i/8]>>uint(7-i&7))&1) + } + for i := hi; i < nd*8; i++ { + bb.fix(uint(i), (bdata[i/8]>>uint(7-i&7))&1) + } + + // Collect editable bits, sorted by visual importance. + type candidate struct { + globalOff int + priority int + } + candidates := make([]candidate, 0, (hi-lo)+nc*8) + for i := lo; i < hi; i++ { + candidates = append(candidates, candidate{doff + i, pixByOff[doff+i].contrast}) + } + for i := 0; i < nc*8; i++ { + candidates = append(candidates, candidate{plan.DataBytes*8 + coff + i, pixByOff[plan.DataBytes*8+coff+i].contrast}) + } + sort.Slice(candidates, func(i, j int) bool { + return candidates[i].priority > candidates[j].priority + }) + + // Try to set each bit to match target brightness. + for _, cand := range candidates { + pi := &pixByOff[cand.globalOff] + desired := byte(1) // dark target → black pixel + if pi.targ >= 128 { + desired = 0 + } + if pi.pix&coding.Invert != 0 { + desired ^= 1 + } + if pi.hardZero { + desired = 0 + } + + var localBit int + if pi.pix.Role() == coding.Data { + localBit = cand.globalOff - doff + } else { + localBit = cand.globalOff - plan.DataBytes*8 - coff + nd*8 + } + bb.trySet(uint(localBit), desired) + } + + bb.writeback() + doff += nd * 8 + coff += nc * 8 + } + + // Extract numeric digits back from the modified data stream. + overflow := false + for i := 0; i < freeBits/10; i++ { + v := 0 + for j := 0; j < 10; j++ { + bi := uint(fixedBits + 10*i + j) + v = v<<1 | int((data[bi/8]>>(7-bi&7))&1) + } + if v >= 1000 { + if hardZeros == nil { + hardZeros = make(map[int]bool) + } + hardZeros[fixedBits+10*i+3] = true + overflow = true + } + num[i*3+0] = byte(v/100 + '0') + num[i*3+1] = byte(v/10%10 + '0') + num[i*3+2] = byte(v%10 + '0') + } + if overflow { + continue + } + + // Final encode with the settled numeric digits. + code, err := plan.Encode(coding.String(urlStr), coding.Num(coding.Num(num))) + if err != nil { + return nil, err + } + + qrCode := &qr.Code{Bitmap: code.Bitmap, Size: code.Size, Stride: code.Stride, Scale: scale} + return qrCode.PNG(), nil + } + + return nil, fmt.Errorf("could not settle numeric encoding after retries") +} + +// --------------------------------------------------------------------------- +// BitBlock — GF(2) linear system for choosing ECC-consistent bit values. +// +// A QR ECC block has nd data bytes and nc check bytes related by Reed-Solomon +// coding. Flipping a data bit deterministically changes certain check bits. +// We model this as a matrix over GF(2): each data bit has a row showing which +// bytes change when it's toggled. Gaussian elimination lets us find a set of +// independent bits we can freely assign while keeping the block valid. +// --------------------------------------------------------------------------- + +type bitBlock struct { + nd, nc int + buf []byte // current data+check bytes (nd+nc) + rows [][]byte // unconsumed basis rows (Gaussian elimination workspace) + used [][]byte // rows that have been consumed (for reset) + rs *gf256.RSEncoder + bdata []byte // slice into the original data array + cdata []byte // slice into the original check array +} + +func newBitBlock(nd, nc int, rs *gf256.RSEncoder, bdata, cdata []byte) *bitBlock { + bb := &bitBlock{ + nd: nd, + nc: nc, + buf: make([]byte, nd+nc), + rows: make([][]byte, 0, nd*8), + rs: rs, + bdata: bdata, + cdata: cdata, + } + + // Initialize buf with current data and compute its check bytes. + copy(bb.buf, bdata) + rs.ECC(bb.buf[:nd], bb.buf[nd:]) + + // Build the basis matrix: for each data bit, compute the effect of + // toggling that bit on the full data+check byte vector. + for i := 0; i < nd*8; i++ { + row := make([]byte, nd+nc) + row[i/8] = 1 << (7 - uint(i%8)) + rs.ECC(row[:nd], row[nd:]) + bb.rows = append(bb.rows, row) + } + + return bb +} + +// fix locks a data bit to a specific value, consuming one degree of freedom. +func (bb *bitBlock) fix(bit uint, val byte) { + bb.trySet(bit, val) +} + +// trySet attempts to set a bit to val using Gaussian elimination. +// Finds a row with a 1 in the target column, eliminates that column from all +// other rows, then applies the row to buf if needed. +func (bb *bitBlock) trySet(bit uint, val byte) bool { + byteIdx, bitMask := bit/8, byte(1<<(7-bit&7)) + + // Find a row with a 1 in this column. + found := -1 + for i, row := range bb.rows { + if row[byteIdx]&bitMask != 0 { + found = i + break + } + } + if found < 0 { + return (bb.buf[byteIdx]>>(7-bit&7))&1 == val + } + + // Move pivot to front. + bb.rows[0], bb.rows[found] = bb.rows[found], bb.rows[0] + pivot := bb.rows[0] + + // Eliminate this column from all other rows. + for _, row := range bb.rows[1:] { + if row[byteIdx]&bitMask != 0 { + xorBytes(row, pivot) + } + } + for _, row := range bb.used { + if row[byteIdx]&bitMask != 0 { + xorBytes(row, pivot) + } + } + + // Apply if needed. + if (bb.buf[byteIdx]>>(7-bit&7))&1 != val { + xorBytes(bb.buf, pivot) + } + + // Consume the pivot. + bb.used = append(bb.used, pivot) + bb.rows = bb.rows[1:] + return true +} + +// writeback copies solved bytes back to the original arrays. +func (bb *bitBlock) writeback() { + copy(bb.bdata, bb.buf[:bb.nd]) + copy(bb.cdata, bb.buf[bb.nd:]) +} + +func xorBytes(dst, src []byte) { + for i := range dst { + dst[i] ^= src[i] + } +} + +// --------------------------------------------------------------------------- +// Image processing +// --------------------------------------------------------------------------- + +// imageToTarget decodes an image and converts it to a grayscale grid at the +// given resolution, returning brightness values (0-255, -1 for transparent). +func imageToTarget(data []byte, size int) ([][]int, error) { + src, _, err := image.Decode(bytes.NewReader(data)) + if err != nil { + return nil, err + } + + b := src.Bounds() + tw, th := size, size + if b.Dx() > b.Dy() { + th = b.Dy() * tw / b.Dx() + } else { + tw = b.Dx() * th / b.Dy() + } + + grid := make([][]int, size) + for y := range grid { + row := make([]int, size) + for x := range row { + row[x] = -1 + } + grid[y] = row + } + + for y := 0; y < th; y++ { + for x := 0; x < tw; x++ { + sx := b.Min.X + x*b.Dx()/tw + sy := b.Min.Y + y*b.Dy()/th + r, g, bl, a := src.At(sx, sy).RGBA() + if a == 0 { + continue + } + lum := (299*uint32(r>>8) + 587*uint32(g>>8) + 114*uint32(bl>>8) + 500) / 1000 + grid[y][x] = int(lum) + } + } + return grid, nil +} + +// targetAt returns brightness and local contrast for a pixel. +func targetAt(target [][]int, x, y int) (brightness byte, contrast int) { + if y < 0 || y >= len(target) || x < 0 || x >= len(target[y]) { + return 255, -1 + } + v := target[y][x] + if v < 0 { + return 255, -1 + } + + n, sum, sumsq := 0, 0, 0 + const radius = 5 + for dy := -radius; dy <= radius; dy++ { + for dx := -radius; dx <= radius; dx++ { + ny, nx := y+dy, x+dx + if ny >= 0 && ny < len(target) && nx >= 0 && nx < len(target[ny]) && target[ny][nx] >= 0 { + val := target[ny][nx] + sum += val + sumsq += val * val + n++ + } + } + } + if n == 0 { + return byte(v), 0 + } + avg := sum / n + return byte(v), sumsq/n - avg*avg +} + +// rotatePixels rotates the QR plan's pixel grid by rot quarter turns. +func rotatePixels(plan *coding.Plan, rot int) { + rot = rot % 4 + if rot == 0 { + return + } + n := len(plan.Pixel) + dst := make([][]coding.Pixel, n) + for i := range dst { + dst[i] = make([]coding.Pixel, n) + } + for y := 0; y < n; y++ { + for x := 0; x < n; x++ { + switch rot { + case 1: + dst[y][x] = plan.Pixel[x][n-1-y] + case 2: + dst[y][x] = plan.Pixel[n-1-y][n-1-x] + case 3: + dst[y][x] = plan.Pixel[n-1-x][y] + } + } + } + plan.Pixel = dst +} + +// --------------------------------------------------------------------------- +// Embedded web UI +// --------------------------------------------------------------------------- + +const indexHTML = ` + +
+ +