diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 995330ca..bc6ec626 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -41,6 +41,7 @@ jobs: uses: actions/setup-go@v4 with: go-version: '>=1.19.0' + cache-dependency-path: tools/**/go.sum - name: Build everything run: make - name: Upload artifacts diff --git a/build/common.mk b/build/common.mk index 3971f3f7..fa227074 100644 --- a/build/common.mk +++ b/build/common.mk @@ -63,6 +63,7 @@ PCHG2C := $(TOPDIR)/tools/pchg2c/pchg2c PNG2C := $(TOPDIR)/tools/png2c.py PSF2C := $(TOPDIR)/tools/psf2c.py SYNC2C := $(TOPDIR)/tools/sync2c/sync2c +SVG2C := $(TOPDIR)/tools/svg2c/svg2c STRIP := m68k-amigaos-strip -s OBJCOPY := m68k-amigaos-objcopy diff --git a/build/effect.mk b/build/effect.mk index adfc275e..0429d797 100644 --- a/build/effect.mk +++ b/build/effect.mk @@ -63,6 +63,10 @@ data/%.c: data/%.png @echo "[PNG] $(DIR)$< -> $(DIR)$@" $(PNG2C) $(PNG2C.$*) $< > $@ || (rm -f $@ && exit 1) +data/%.c: data/%.svg + @echo "[SVG] $(DIR)$< -> $(DIR)$@" + $(SVG2C) $(SVG2C.$*) -o $@ $< + data/%.c: data/%.2d @echo "[2D] $(DIR)$< -> $(DIR)$@" $(CONV2D) $(CONV2D.$*) $< > $@ || (rm -f $@ && exit 1) diff --git a/tools/Makefile b/tools/Makefile index 598f47c6..11a61c6f 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -1,5 +1,5 @@ TOPDIR := $(realpath ..) -SUBDIRS := dumphunk dumpilbm maketmx pchg2c ptdump sync2c tmxconv +SUBDIRS := dumphunk dumpilbm maketmx pchg2c ptdump sync2c tmxconv svg2c include $(TOPDIR)/build/common.mk diff --git a/tools/svg2c/.gitignore b/tools/svg2c/.gitignore new file mode 100644 index 00000000..e0fad57e --- /dev/null +++ b/tools/svg2c/.gitignore @@ -0,0 +1,2 @@ +svg2c +svg2c.exe diff --git a/tools/svg2c/Makefile b/tools/svg2c/Makefile new file mode 100644 index 00000000..dead18a9 --- /dev/null +++ b/tools/svg2c/Makefile @@ -0,0 +1,3 @@ +TOPDIR := $(realpath ../..) + +include $(TOPDIR)/build/go.mk diff --git a/tools/svg2c/go.mod b/tools/svg2c/go.mod new file mode 100644 index 00000000..c751834c --- /dev/null +++ b/tools/svg2c/go.mod @@ -0,0 +1,9 @@ +module ghostown.pl/svg2c + +go 1.17 + +require ( + github.com/joshvarga/svgparser v0.0.0-20200804023048-5eaba627a7d1 // indirect + golang.org/x/net v0.9.0 // indirect + golang.org/x/text v0.9.0 // indirect +) diff --git a/tools/svg2c/go.sum b/tools/svg2c/go.sum new file mode 100644 index 00000000..846a6795 --- /dev/null +++ b/tools/svg2c/go.sum @@ -0,0 +1,6 @@ +github.com/joshvarga/svgparser v0.0.0-20200804023048-5eaba627a7d1 h1:nv2n9UzqqLLe40j2YpqwVotVtugdHbFmGS4vm9S/LTs= +github.com/joshvarga/svgparser v0.0.0-20200804023048-5eaba627a7d1/go.mod h1:L7A8o4juotPcM0GC/A1DV4tjbRgp9QCsC6hmmoljed0= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= diff --git a/tools/svg2c/main.go b/tools/svg2c/main.go new file mode 100644 index 00000000..e03bc7e1 --- /dev/null +++ b/tools/svg2c/main.go @@ -0,0 +1,328 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "html/template" + "log" + "math" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + + "github.com/joshvarga/svgparser" +) + +const ( + svgTemplate = `static GreetsT {{ .Name }} = { + .curr = NULL, + .n = 0, + .x = 0, + .y = 0, + .origin_x = {{ .Origin.X }}, + .origin_y = {{ .Origin.Y }}, + .delay = {{ .Delay }}, + .data = { + {{- range .Data }} + {{ .Length }}, + {{- range .DeltaPoints }}{{ .X }},{{.Y}},{{- end }} + {{- end }} + -1 + } +}; +` +) + +// A SVG data container for a single file +type SvgData struct { + Data []GeometricData // Individual SVG elements data + Origin Point // Origin point for whole SVG + Delay int // Delay in frames + Name string // Name of the data (file name) +} + +// A SVG data for individual SVG element eg. line, polyline +type GeometricData struct { + Length int // Length of the points slice + Points []Point // Standard points representation + DeltaPoints []Point // Delta encoded points representation +} + +type Point struct { + X int + Y int +} + +func Add(p1, p2 Point) (p Point) { + return Point{p1.X + p2.X, p1.Y + p2.Y} +} + +func Sub(p1, p2 Point) (p Point) { + return Point{p1.X - p2.X, p1.Y - p2.Y} +} + +// parseCoordinate parses string float representation to integer eg. "123.45" -> 123 +func parseCoordinate(coord string) (int, error) { + num, err := strconv.ParseFloat(coord, 32) + if err != nil { + return 0, err + } + return int(math.Round(num)), nil +} + +// parseLine parses SVG line element and returns slice (pair) of Points +// line element has explicitly defined attributes x1, y1, x2, y2 +func parseLine(el *svgparser.Element) []Point { + X1, err := parseCoordinate(el.Attributes["x1"]) + if err != nil { + log.Fatalf("Error parsing line coord x1 - value: %s\n", + el.Attributes["x1"]) + } + Y1, err := parseCoordinate(el.Attributes["y1"]) + if err != nil { + log.Fatalf("Error parsing coordinates y1 - value: %s\n", + el.Attributes["y1"]) + } + X2, err := parseCoordinate(el.Attributes["x2"]) + if err != nil { + log.Fatalf("Error parsing coordinates x2 - value: %s\n", + el.Attributes["x2"]) + } + Y2, err := parseCoordinate(el.Attributes["y2"]) + if err != nil { + log.Fatalf("Error parsing coordinates y2 - value: %s\n", + el.Attributes["y2"]) + } + return []Point{{X1, Y1}, {X2, Y2}} +} + +// parsePolyline parses polyline SVG element and returns slice of elements +// polyline holds points data in "x1,y1 x2,y2 ... xn,yn" format +func parsePolyLine(el *svgparser.Element) []Point { + pointsString := el.Attributes["points"] + space := regexp.MustCompile(`\s+`) + trimmedPoints := space.ReplaceAllString(pointsString, " ") + points := strings.Split(trimmedPoints, " ") + var parsedPoints = make([]Point, 0, len(points)) + for _, point := range points { + if point == "" { + continue + } + coords := strings.Split(point, ",") + x, err := parseCoordinate(strings.TrimSpace(coords[0])) + if err != nil { + log.Fatalf("Error parsing coordinate x in point: '%v'\n", point) + } + y, err := parseCoordinate(coords[1]) + if err != nil { + log.Fatalf("Error parsing coordinate y in point: '%v'\n", point) + } + parsedPoint := Point{x, y} + parsedPoints = append(parsedPoints, parsedPoint) + } + return parsedPoints +} + +// calcOrigin calculates coordinates of the point which is +// in the middle of the SVG bounding box from the top-left corner of the screen +func (sd *SvgData) calcOrigin() { + minPoint := sd.getMinPoint() + maxPoint := sd.getMaxPoint() + midPoint := Point{ + (maxPoint.X - minPoint.X) / 2, + (maxPoint.Y - minPoint.Y) / 2, + } + sd.Origin = Add(minPoint, midPoint) +} + +func (sd *SvgData) calcDataWithOffset() { + for idx, gData := range sd.Data { + sd.Data[idx].DeltaPoints = gData.getWithOffsetAndDelta(sd.Origin) + } +} + +// getMinPoint calculates top-left corner of the SVG bounding box, +// the closest point to the top-left corner of the screen. +func (sd *SvgData) getMinPoint() Point { + var minPoint = sd.Data[0].Points[0] + for _, gData := range sd.Data { + for _, point := range gData.Points { + if point.X < minPoint.X { + minPoint.X = point.X + } + if point.Y < minPoint.Y { + minPoint.Y = point.Y + } + } + } + return minPoint +} + +// getMaxPoint calculates bottom-right corner of the SVG bounding box, +// the farthest point to the top-left corner of the screen. +func (sd *SvgData) getMaxPoint() Point { + var maxPoint = sd.Data[0].Points[0] + for _, gData := range sd.Data { + for _, point := range gData.Points { + if point.X > maxPoint.X { + maxPoint.X = point.X + } + if point.Y > maxPoint.Y { + maxPoint.Y = point.Y + } + } + } + return maxPoint +} + +// Export returns string data of the SvgData structure +func (sd *SvgData) Export() (output string, err error) { + t, err := template.New("svg").Parse(svgTemplate) + if err != nil { + return + } + + file, err := os.Create(outputPath) + if err != nil { + return "", err + } + defer file.Close() + + var data bytes.Buffer + err = t.Execute(&data, sd) + if err != nil { + return "", err + } + return data.String(), nil +} + +// getWithOffsetAndDelta returns points with offset compressed with delta compression, +func (gd *GeometricData) getWithOffsetAndDelta(offset Point) []Point { + inputPoints := gd.getPointsWithOffset(offset) + deltaPoints := []Point{inputPoints[0]} + for index, point := range inputPoints { + if index != 0 { + lastPoint := inputPoints[index-1] + deltaPoints = append(deltaPoints, Sub(point, lastPoint)) + } + } + return deltaPoints +} + +// getPointsWithOffset returns points translated by the offset +func (gd *GeometricData) getPointsWithOffset(offset Point) []Point { + var withOffset []Point + for _, point := range gd.Points { + withOffset = append(withOffset, Sub(point, offset)) + } + return withOffset +} + +// handleFile returns file data as converted string +func handleFile(file *os.File, name string, delay int) string { + svgData := SvgData{[]GeometricData{}, Point{0, 0}, delay, name} + + element, err := svgparser.Parse(file, false) + if err != nil { + log.Fatal(err) + } + elements := element.Children + + for _, element := range elements { + elementType := element.Name + switch elementType { + case "polyline": + parsedPoints := parsePolyLine(element) + svgData.Data = append(svgData.Data, + GeometricData{len(parsedPoints), parsedPoints, []Point{}}) + case "line": + parsedPoints := parseLine(element) + svgData.Data = append(svgData.Data, + GeometricData{len(parsedPoints), parsedPoints, []Point{}}) + case "path": + if verbose { + fmt.Println("Found path element, geometry won't be parsed") + } + default: + if verbose { + fmt.Printf("[WARN] Parsed element %s\n", element.Name) + } + } + } + svgData.calcOrigin() + svgData.calcDataWithOffset() + data, err := svgData.Export() + if err != nil { + log.Fatal(err) + } + return data +} + +const defaultFileName = "data.c" + +var outputPath, structName string +var verbose, printHelp bool +var delayValue int + +func init() { + flag.StringVar(&outputPath, "o", defaultFileName, "Output filename with processed data") + flag.StringVar(&structName, "name", "", "Custom structure name") + flag.BoolVar(&verbose, "v", false, "Output verbose logs") + flag.IntVar(&delayValue, "delay", 0, "Delay in frames") + flag.BoolVar(&printHelp, "help", false, "Prints this message") +} + +func main() { + + flag.Parse() + + if printHelp || len(flag.Args()) < 1 || len(outputPath) == 0 { + flag.PrintDefaults() + os.Exit(1) + } + + var exportString string + + if len(flag.Args()) != 1 { + log.Fatalln("svg2c handles only single file conversion at once") + } + + fileName := flag.Args()[0] + + file, err := os.Open(fileName) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + fileExt := filepath.Ext(fileName) + fileBase := filepath.Base(fileName) + if len(structName) == 0 { + structName = strings.TrimSuffix(fileBase, fileExt) + structName = strings.Replace(structName, "-", "_", -1) + } + + if fileExt == ".svg" { + if verbose { + fmt.Println("Converting file:", fileName) + } + exportString += handleFile(file, structName, delayValue) + } + + if len(exportString) == 0 { + os.Exit(0) + } + + outFile, err := os.Create(outputPath) + if err != nil { + log.Fatal(err) + } + defer outFile.Close() + outFile.WriteString(exportString) + if err != nil { + log.Fatal(err) + } +}