Files
labels/main.go
T

146 lines
4.1 KiB
Go

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, `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%s width="%gin" height="%gin" viewBox="0 0 %g %g">`+"\n", src.nsAttrs, pageWidth, pageHeight, pageWidth*pt, pageHeight*pt)
fmt.Fprintf(&b, `<defs><symbol id="labels-flag" viewBox="%g %g %g %g" preserveAspectRatio="none"><g%s>%s</g></symbol></defs>`+"\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, `<circle cx="%g" cy="%g" r="%g" fill="none" stroke="black" stroke-width="0.5" stroke-dasharray="2 1.5"/>`+"\n", cx, cy, circleDiameter/2*pt)
fmt.Fprintf(&b, `<use xlink:href="#labels-flag" x="%g" y="%g" width="%g" height="%g"/>`+"\n", cx-flagW/2*pt, cy-flagH/2*pt, flagW*pt, flagH*pt)
if *stroke {
fmt.Fprintf(&b, `<rect x="%g" y="%g" width="%g" height="%g" fill="none" stroke="black" stroke-width="0.25"/>`+"\n", cx-flagW/2*pt, cy-flagH/2*pt, flagW*pt, flagH*pt)
}
}
}
b.WriteString("</svg>\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("</"))
if innerEnd < innerStart {
log.Fatal("[ERROR] svg root element has no closing tag")
}
f.inner = string(data[innerStart:innerEnd])
return f
}
}
func parseLength(s string) float64 {
v, err := strconv.ParseFloat(strings.TrimRight(strings.TrimSpace(s), "abcdefghijklmnopqrstuvwxyz%"), 64)
if err != nil {
log.Fatalf("[ERROR] bad svg dimension %q", s)
}
return v
}
func escape(s string) string {
var b strings.Builder
xml.EscapeText(&b, []byte(s))
return b.String()
}