From 518e240bca5bf7c36ecc40bf4b3934d7a5de8929 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Wed, 23 Sep 2026 21:14:00 -0700 Subject: [PATCH] PLS312 flag label sheet generator --- .gitignore | 1 + go.mod | 3 ++ main.go | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89f9ac0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +out/ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2437fe0 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module labels + +go 1.26.7 diff --git a/main.go b/main.go new file mode 100644 index 0000000..4567bba --- /dev/null +++ b/main.go @@ -0,0 +1,145 @@ +package main + +import ( + "bytes" + "encoding/xml" + "flag" + "fmt" + "log" + "math" + "os" + "os/exec" + "strconv" + "strings" +) + +const ( + pt = 72.0 + pageWidth = 8.5 + pageHeight = 11.0 + cols = 7 + rows = 9 + pitch = 1.125 + labelDiameter = 1.0 + leftMargin = 0.375 + topMargin = 0.5 + circleDiameter = 0.875 + flagDiagonal = 0.8 +) + +var rootOnlyAttrs = map[string]bool{ + "width": true, "height": true, "x": true, "y": true, "viewBox": true, + "preserveAspectRatio": true, "version": true, "baseProfile": true, +} + +type flagSVG struct { + viewBox [4]float64 + nsAttrs string + gAttrs string + inner string +} + +func main() { + stroke := flag.Bool("stroke", false, "draw a thin black stroke around each flag") + flag.Parse() + if flag.NArg() != 2 { + log.Fatal("usage: go run . [--stroke] flag.svg out.pdf") + } + + data, err := os.ReadFile(flag.Arg(0)) + if err != nil { + log.Fatal(err) + } + src := parseFlag(data) + + aspect := src.viewBox[2] / src.viewBox[3] + flagH := flagDiagonal / math.Sqrt(1+aspect*aspect) + flagW := aspect * flagH + + var b strings.Builder + fmt.Fprintf(&b, ``+"\n", src.nsAttrs, pageWidth, pageHeight, pageWidth*pt, pageHeight*pt) + fmt.Fprintf(&b, `%s`+"\n", src.viewBox[0], src.viewBox[1], src.viewBox[2], src.viewBox[3], src.gAttrs, src.inner) + for r := 0; r < rows; r++ { + for c := 0; c < cols; c++ { + cx := (leftMargin + labelDiameter/2 + float64(c)*pitch) * pt + cy := (topMargin + labelDiameter/2 + float64(r)*pitch) * pt + fmt.Fprintf(&b, ``+"\n", cx, cy, circleDiameter/2*pt) + fmt.Fprintf(&b, ``+"\n", cx-flagW/2*pt, cy-flagH/2*pt, flagW*pt, flagH*pt) + if *stroke { + fmt.Fprintf(&b, ``+"\n", cx-flagW/2*pt, cy-flagH/2*pt, flagW*pt, flagH*pt) + } + } + } + b.WriteString("\n") + + cmd := exec.Command("rsvg-convert", "--format", "pdf", "--output", flag.Arg(1)) + cmd.Stdin = strings.NewReader(b.String()) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + log.Fatal(err) + } +} + +func parseFlag(data []byte) flagSVG { + dec := xml.NewDecoder(bytes.NewReader(data)) + for { + tok, err := dec.Token() + if err != nil { + log.Fatalf("[ERROR] no svg root element: %v", err) + } + start, ok := tok.(xml.StartElement) + if !ok { + continue + } + + f := flagSVG{} + attrs := map[string]string{} + for _, a := range start.Attr { + switch { + case a.Name.Space == "xmlns" && a.Name.Local != "xlink": + f.nsAttrs += fmt.Sprintf(` xmlns:%s="%s"`, a.Name.Local, escape(a.Value)) + case a.Name.Space == "" && a.Name.Local != "xmlns" && !rootOnlyAttrs[a.Name.Local]: + f.gAttrs += fmt.Sprintf(` %s="%s"`, a.Name.Local, escape(a.Value)) + } + attrs[a.Name.Local] = a.Value + } + + if vb, ok := attrs["viewBox"]; ok { + fields := strings.FieldsFunc(vb, func(r rune) bool { return r == ' ' || r == ',' }) + if len(fields) != 4 { + log.Fatalf("[ERROR] bad viewBox %q", vb) + } + for i, s := range fields { + f.viewBox[i] = parseLength(s) + } + } else { + f.viewBox = [4]float64{0, 0, parseLength(attrs["width"]), parseLength(attrs["height"])} + } + if f.viewBox[2] <= 0 || f.viewBox[3] <= 0 { + log.Fatalf("[ERROR] bad svg size %v", f.viewBox) + } + + innerStart := int(dec.InputOffset()) + innerEnd := bytes.LastIndex(data, []byte("