146 lines
4.1 KiB
Go
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()
|
|
}
|