mirror of
https://github.com/mineracks/seedhammer-v1-companion.git
synced 2026-06-26 22:01:05 +10:00
Twelve packages lifted from seedhammer/seedhammer @ v1.3.0
(commit 2f071c1d8f23eb7fd39b15fc0acb8874113f801e):
address/ — bitcoin address parsing
backup/ — v1 plate dimensions + UR-coded multi-plate backup
bc/ — Blockchain Commons: ur, fountain, bytewords, urtypes,
xoshiro256 (5 subpkgs)
bip32/ — HD-key derivation
bip39/ — mnemonic seed phrases + 2048-word wordlist
engrave/ — text/QR → MoveTo/LineTo command stream conversion
seedqr/ — SeedQR / CompactSeedQR encoders
image/ — paletted, rgb565, alpha4, ninepatch image formats
nonstandard/ — bitcoin descriptor + script parsing
font/ — bitmap + vector font runtime
font/{comfortaa,poppins,constant,bitmap,vector}/ — actual fonts
driver/mjolnir/ — MarkingWay USB-serial engraver driver
Plus an earlier-aside backup_test.go restored (its deps are now lifted).
Import paths globally rewritten seedhammer.com → mineracks namespace
via single sed pass; verified no orphan refs remain. go.mod adopts
upstream's full dep set plus the replace-directive for the patched
kortschak/qr fork.
go build ./... clean (all 27 packages)
go test ./... clean (12 packages with tests, all passing)
NOT lifted in this commit:
- driver/{wshat,drm,libcamera} (hardware-specific GPIO/LCD/camera —
will be platform-v1/-shaped abstractions instead)
- gui/ (depends on the above; lifts in Phase 2)
- cmd/{controller,...} (Pi binary entrypoints — not needed for the
companion repo)
- zbar/ (QR scanner — needs libcamera)
Next:
- Write the SH1E reference encoder/decoder in engrave/wire/sh1e/
- Lift Gangleri42's cmd/webnfc/ shell + retune to v1 plates
- First buildable composer WASM with a working preview
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
491 lines
10 KiB
Go
491 lines
10 KiB
Go
//go:build ignore
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/xml"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"go/format"
|
|
"image"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/mineracks/seedhammer-v1-companion/font/vector"
|
|
)
|
|
|
|
var packageName = flag.String("package", "main", "package name")
|
|
|
|
type Face struct {
|
|
Metrics vector.Metrics
|
|
// Index maps a character to its segment range.
|
|
Index [unicode.MaxASCII]vector.Glyph
|
|
// Segments encoded as opcode, args, opcode, args...
|
|
Segments []byte
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
if flag.NArg() != 2 {
|
|
fmt.Fprintf(os.Stderr, "usage: convert infile outfile\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
infile := flag.Arg(0)
|
|
in, err := os.ReadFile(infile)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
conv, err := convert(in)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "failed to parse %q: %v\n", infile, err)
|
|
os.Exit(1)
|
|
}
|
|
if err := generate(filepath.Base(infile), conv); err != nil {
|
|
fmt.Fprintf(os.Stderr, "failed to generate %q: %v\n", infile, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func generate(fname string, conv *Face) error {
|
|
var output bytes.Buffer
|
|
ext := filepath.Ext(fname)
|
|
fname = fname[:len(fname)-len(ext)]
|
|
name := flag.Arg(1)
|
|
fmt.Fprintf(&output, "// Code generated by font/vector/convert.go; DO NOT EDIT.\npackage %s\n", *packageName)
|
|
fmt.Fprintf(&output, "import (\n")
|
|
fmt.Fprintf(&output, " _ \"embed\"\n")
|
|
fmt.Fprintf(&output, " \"unsafe\"\n")
|
|
fmt.Fprintf(&output, " \"github.com/mineracks/seedhammer-v1-companion/font/vector\"\n")
|
|
fmt.Fprintf(&output, ")\n\n")
|
|
fmt.Fprintf(&output, "var Font = vector.NewFace(unsafe.Slice(unsafe.StringData(%sData), len(%[1]sData)))\n\n", name)
|
|
fmt.Fprintf(&output, "//go:embed %s.bin\n", name)
|
|
fmt.Fprintf(&output, "var %sData string\n", name)
|
|
formatted, err := format.Source(output.Bytes())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var data []byte
|
|
bo := binary.LittleEndian
|
|
data = append(data, uint8(conv.Metrics.Ascent), uint8(conv.Metrics.Height))
|
|
for _, g := range conv.Index {
|
|
data = append(data, uint8(g.Advance))
|
|
start, end := int(g.Start)+vector.OffSegments, int(g.End)+vector.OffSegments
|
|
s16, e16 := uint16(start), uint16(end)
|
|
if int(s16) != start || int(e16) != end {
|
|
return errors.New("segment offset overflows uint16")
|
|
}
|
|
data = bo.AppendUint16(data, s16)
|
|
data = bo.AppendUint16(data, e16)
|
|
}
|
|
if len(data) != vector.OffSegments {
|
|
panic("miscalculated segment offset")
|
|
}
|
|
data = append(data, conv.Segments...)
|
|
if err := os.WriteFile(name+".go", formatted, 0o600); err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(name+".bin", data, 0o600)
|
|
}
|
|
|
|
type MetaData struct {
|
|
Advance, Height, Baseline int
|
|
}
|
|
|
|
func convert(svg []byte) (*Face, error) {
|
|
d := xml.NewDecoder(bytes.NewReader(svg))
|
|
for {
|
|
root, err := d.Token()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t, ok := root.(xml.StartElement)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if !ok || t.Name.Local != "svg" {
|
|
return nil, errors.New("missing <svg> root element")
|
|
}
|
|
face := new(Face)
|
|
meta, err := parseMeta(svg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ascent := int8(meta.Baseline)
|
|
if int(ascent) != meta.Baseline {
|
|
return nil, errors.New("baseline overflows int8")
|
|
}
|
|
height := int8(meta.Height)
|
|
if int(height) != meta.Height {
|
|
return nil, errors.New("height overlflows int8")
|
|
}
|
|
face.Metrics.Ascent = ascent
|
|
face.Metrics.Height = height
|
|
adv := meta.Advance
|
|
face.Index[' '] = vector.Glyph{
|
|
Advance: int8(adv),
|
|
}
|
|
err = parseChars(face, d, adv, int(ascent))
|
|
return face, err
|
|
}
|
|
}
|
|
|
|
func parseMeta(data []byte) (*MetaData, error) {
|
|
type Line struct {
|
|
ID string `xml:"id,attr"`
|
|
X1 float64 `xml:"x1,attr"`
|
|
Y1 float64 `xml:"y1,attr"`
|
|
X2 float64 `xml:"x2,attr"`
|
|
Y2 float64 `xml:"y2,attr"`
|
|
}
|
|
type SVG struct {
|
|
XMLName xml.Name `xml:"svg"`
|
|
Lines []Line `xml:"line"`
|
|
}
|
|
var svg SVG
|
|
if err := xml.Unmarshal(data, &svg); err != nil {
|
|
return nil, err
|
|
}
|
|
var meta MetaData
|
|
for _, line := range svg.Lines {
|
|
switch line.ID {
|
|
case "advance":
|
|
meta.Advance = mustInt(line.X2 - line.X1)
|
|
case "height":
|
|
meta.Height = mustInt(line.Y2 - line.Y1)
|
|
case "baseline":
|
|
meta.Baseline = mustInt(line.Y1)
|
|
}
|
|
}
|
|
return &meta, nil
|
|
}
|
|
|
|
func findAttr(e xml.StartElement, name string) (string, bool) {
|
|
for _, a := range e.Attr {
|
|
if a.Name.Local == name {
|
|
return a.Value, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func mustInt(v float64) int {
|
|
i := int(v)
|
|
if float64(i) != v {
|
|
panic("non-integer floating point number")
|
|
}
|
|
return i
|
|
}
|
|
|
|
func parseChars(face *Face, d *xml.Decoder, adv, ascent int) error {
|
|
offx := 0
|
|
for {
|
|
t, err := d.Token()
|
|
if err != nil {
|
|
if err != io.EOF {
|
|
return err
|
|
}
|
|
break
|
|
}
|
|
e, ok := t.(xml.StartElement)
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch e.Name.Local {
|
|
case "style":
|
|
if err := d.Skip(); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
id, _ := findAttr(e, "id")
|
|
switch id {
|
|
case "advance", "height", "baseline", "size":
|
|
// Skip anonymous and meta-data elements.
|
|
if err := d.Skip(); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
r, ok := mapChar(id)
|
|
if !ok {
|
|
return fmt.Errorf("unknown character id: %q", id)
|
|
}
|
|
idxStart := len(face.Segments)
|
|
if err := parseSegments(face, d, e, offx, -ascent); err != nil {
|
|
return err
|
|
}
|
|
idxEnd := len(face.Segments)
|
|
face.Index[r] = vector.Glyph{
|
|
Advance: int8(adv),
|
|
Start: uint16(idxStart),
|
|
End: uint16(idxEnd),
|
|
}
|
|
offx -= adv
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseSegments(face *Face, d *xml.Decoder, e xml.StartElement, offx, offy int) error {
|
|
encode := func(op vector.SegmentOp, args ...image.Point) {
|
|
face.Segments = append(face.Segments, byte(op))
|
|
for _, a := range args {
|
|
x, y := int8(a.X), int8(a.Y)
|
|
if int(x) != a.X || int(y) != a.Y {
|
|
panic(fmt.Errorf("coordinates out of range: %v", a))
|
|
}
|
|
face.Segments = append(face.Segments, byte(x), byte(y))
|
|
}
|
|
}
|
|
switch n := e.Name.Local; n {
|
|
case "g":
|
|
for {
|
|
t, err := d.Token()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch t := t.(type) {
|
|
case xml.StartElement:
|
|
if err := parseSegments(face, d, t, offx, offy); err != nil {
|
|
return err
|
|
}
|
|
case xml.EndElement:
|
|
return nil
|
|
}
|
|
}
|
|
case "line":
|
|
var line struct {
|
|
X1 float64 `xml:"x1,attr"`
|
|
Y1 float64 `xml:"y1,attr"`
|
|
X2 float64 `xml:"x2,attr"`
|
|
Y2 float64 `xml:"y2,attr"`
|
|
}
|
|
if err := d.DecodeElement(&line, &e); err != nil {
|
|
return err
|
|
}
|
|
encode(vector.SegmentOpMoveTo, image.Pt(mustInt(line.X1)+offx, mustInt(line.Y1)+offy))
|
|
encode(vector.SegmentOpLineTo, image.Pt(mustInt(line.X2)+offx, mustInt(line.Y2)+offy))
|
|
return nil
|
|
case "polyline":
|
|
points, ok := findAttr(e, "points")
|
|
if !ok {
|
|
return errors.New("missing points attribute for <polyline>")
|
|
}
|
|
points = strings.TrimSpace(points)
|
|
coords := strings.Split(points, " ")
|
|
for i, c := range coords {
|
|
var x, y float64
|
|
if _, err := fmt.Sscanf(c, "%f,%f", &x, &y); err != nil {
|
|
return fmt.Errorf("invalid coordinates %q in <polyline>:", c)
|
|
}
|
|
op := vector.SegmentOpLineTo
|
|
if i == 0 {
|
|
op = vector.SegmentOpMoveTo
|
|
}
|
|
encode(op, image.Pt(mustInt(x)+offx, mustInt(y)+offy))
|
|
}
|
|
return d.Skip()
|
|
case "path":
|
|
cmds, ok := findAttr(e, "d")
|
|
if !ok {
|
|
return errors.New("missing d attribute for <path>")
|
|
}
|
|
cmds = strings.TrimSpace(cmds)
|
|
pen := image.Pt(offx, offy)
|
|
initPoint := pen
|
|
ctrl2 := pen
|
|
for {
|
|
cmds = strings.TrimLeft(cmds, " ,\t\n")
|
|
if len(cmds) == 0 {
|
|
break
|
|
}
|
|
orig := cmds
|
|
op := rune(cmds[0])
|
|
cmds = cmds[1:]
|
|
switch op {
|
|
case 'M', 'm', 'V', 'v', 'L', 'l', 'H', 'h', 'C', 'c', 'S', 's':
|
|
case 'Z', 'z':
|
|
if pen != initPoint {
|
|
encode(vector.SegmentOpLineTo, initPoint)
|
|
pen = initPoint
|
|
}
|
|
ctrl2 = initPoint
|
|
continue
|
|
default:
|
|
return fmt.Errorf("unknown <path> command %s in %q", string(op), orig)
|
|
}
|
|
var coords []int
|
|
for {
|
|
cmds = strings.TrimLeft(cmds, " ,\t\n")
|
|
if len(cmds) == 0 {
|
|
break
|
|
}
|
|
n, x, ok := parseFloat(cmds)
|
|
if !ok {
|
|
break
|
|
}
|
|
cmds = cmds[n:]
|
|
coords = append(coords, mustInt(x))
|
|
}
|
|
rel := unicode.IsLower(op)
|
|
newPen := pen
|
|
switch unicode.ToLower(op) {
|
|
case 'h':
|
|
for _, x := range coords {
|
|
p := image.Pt(x, pen.Y)
|
|
if rel {
|
|
p.X += pen.X
|
|
} else {
|
|
p.X += offx
|
|
}
|
|
encode(vector.SegmentOpLineTo, p)
|
|
newPen = p
|
|
}
|
|
pen = newPen
|
|
ctrl2 = newPen
|
|
continue
|
|
case 'v':
|
|
for _, y := range coords {
|
|
p := image.Pt(pen.X, y)
|
|
if rel {
|
|
p.Y += pen.Y
|
|
} else {
|
|
p.Y += offy
|
|
}
|
|
encode(vector.SegmentOpLineTo, p)
|
|
newPen = p
|
|
}
|
|
pen = newPen
|
|
ctrl2 = newPen
|
|
continue
|
|
}
|
|
if len(coords)%2 != 0 {
|
|
return fmt.Errorf("odd number of coordinates in <path> data: %q", orig)
|
|
}
|
|
var off image.Point
|
|
if rel {
|
|
// Relative command.
|
|
off = pen
|
|
} else {
|
|
off = image.Pt(offx, offy)
|
|
}
|
|
var points []image.Point
|
|
for i := 0; i < len(coords); i += 2 {
|
|
p := image.Pt(coords[i], coords[i+1])
|
|
p = p.Add(off)
|
|
points = append(points, p)
|
|
}
|
|
newCtrl2 := ctrl2
|
|
switch op := unicode.ToLower(op); op {
|
|
case 'm', 'l':
|
|
sop := vector.SegmentOpMoveTo
|
|
if op == 'l' {
|
|
sop = vector.SegmentOpLineTo
|
|
}
|
|
for _, p := range points {
|
|
encode(sop, p)
|
|
newPen = p
|
|
}
|
|
if op == 'm' {
|
|
initPoint = newPen
|
|
}
|
|
case 'c', 's':
|
|
return errors.New("cubic splines not supported")
|
|
}
|
|
pen = newPen
|
|
ctrl2 = newCtrl2
|
|
}
|
|
return d.Skip()
|
|
default:
|
|
return fmt.Errorf("unsupported element: <%s>", n)
|
|
}
|
|
}
|
|
|
|
func parseFloat(s string) (int, float64, bool) {
|
|
n := 0
|
|
if len(s) > 0 && s[0] == '-' {
|
|
n++
|
|
}
|
|
for ; n < len(s); n++ {
|
|
if !(unicode.IsDigit(rune(s[n])) || s[n] == '.') {
|
|
break
|
|
}
|
|
}
|
|
f, err := strconv.ParseFloat(s[:n], 64)
|
|
return n, f, err == nil
|
|
}
|
|
|
|
func mapChar(id string) (rune, bool) {
|
|
var r rune
|
|
switch {
|
|
case len(id) == 1:
|
|
r = rune(id[0])
|
|
default:
|
|
switch id {
|
|
case "zero":
|
|
r = '0'
|
|
case "one":
|
|
r = '1'
|
|
case "two":
|
|
r = '2'
|
|
case "three":
|
|
r = '3'
|
|
case "four":
|
|
r = '4'
|
|
case "five":
|
|
r = '5'
|
|
case "six":
|
|
r = '6'
|
|
case "seven":
|
|
r = '7'
|
|
case "eight":
|
|
r = '8'
|
|
case "nine":
|
|
r = '9'
|
|
case "colon":
|
|
r = ':'
|
|
case "comma":
|
|
r = ','
|
|
case "slash":
|
|
r = '/'
|
|
case "apostrophe":
|
|
r = '\''
|
|
case "dash":
|
|
r = '-'
|
|
case "period":
|
|
r = '.'
|
|
case "leftparen":
|
|
r = '('
|
|
case "rightparen":
|
|
r = ')'
|
|
case "leftbracket":
|
|
r = '['
|
|
case "rightbracket":
|
|
r = ']'
|
|
case "leftcurlybrace":
|
|
r = '{'
|
|
case "rightcurlybrace":
|
|
r = '}'
|
|
case "hash":
|
|
r = '#'
|
|
case "star":
|
|
r = '*'
|
|
case "at":
|
|
r = '@'
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
return r, true
|
|
}
|