184 lines
6.4 KiB
Go
184 lines
6.4 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// GenerateScanGridSVG creates printable calibration grid SVG files.
|
|
// Returns the list of generated file paths.
|
|
func GenerateScanGridSVG(cfg *ScanHelperConfig, outputDir string) ([]string, error) {
|
|
if cfg == nil {
|
|
return nil, fmt.Errorf("no scan helper config")
|
|
}
|
|
|
|
os.MkdirAll(outputDir, 0755)
|
|
|
|
pageW := cfg.PageWidth
|
|
pageH := cfg.PageHeight
|
|
grid := cfg.GridSpacing
|
|
if grid <= 0 {
|
|
grid = 10
|
|
}
|
|
|
|
margin := 15.0 // mm margin for printer bleed
|
|
markerSize := 5.0
|
|
|
|
var files []string
|
|
|
|
for row := 0; row < cfg.PagesTall; row++ {
|
|
for col := 0; col < cfg.PagesWide; col++ {
|
|
filename := fmt.Sprintf("scan_grid_%dx%d.svg", col+1, row+1)
|
|
if cfg.PagesWide == 1 && cfg.PagesTall == 1 {
|
|
filename = "scan_grid.svg"
|
|
}
|
|
path := filepath.Join(outputDir, filename)
|
|
|
|
svg, err := renderGridPage(pageW, pageH, margin, grid, markerSize, col, row, cfg.PagesWide, cfg.PagesTall)
|
|
if err != nil {
|
|
return files, err
|
|
}
|
|
|
|
if err := os.WriteFile(path, []byte(svg), 0644); err != nil {
|
|
return files, fmt.Errorf("write %s: %v", filename, err)
|
|
}
|
|
files = append(files, path)
|
|
}
|
|
}
|
|
|
|
return files, nil
|
|
}
|
|
|
|
func renderGridPage(pageW, pageH, margin, gridSpacing, markerSize float64, col, row, totalCols, totalRows int) (string, error) {
|
|
var b strings.Builder
|
|
|
|
b.WriteString(fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1"
|
|
width="%.1fmm" height="%.1fmm"
|
|
viewBox="0 0 %.1f %.1f">
|
|
`, pageW, pageH, pageW, pageH))
|
|
|
|
// White background
|
|
b.WriteString(fmt.Sprintf(`<rect x="0" y="0" width="%.1f" height="%.1f" fill="white"/>
|
|
`, pageW, pageH))
|
|
|
|
// Printable area
|
|
areaX := margin
|
|
areaY := margin
|
|
areaW := pageW - 2*margin
|
|
areaH := pageH - 2*margin
|
|
|
|
// Thin border around printable area
|
|
b.WriteString(fmt.Sprintf(`<rect x="%.2f" y="%.2f" width="%.2f" height="%.2f" fill="none" stroke="#ccc" stroke-width="0.3"/>
|
|
`, areaX, areaY, areaW, areaH))
|
|
|
|
// Grid lines
|
|
b.WriteString(`<g stroke="#ddd" stroke-width="0.15">` + "\n")
|
|
for x := areaX; x <= areaX+areaW+0.01; x += gridSpacing {
|
|
b.WriteString(fmt.Sprintf(` <line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f"/>`, x, areaY, x, areaY+areaH))
|
|
b.WriteString("\n")
|
|
}
|
|
for y := areaY; y <= areaY+areaH+0.01; y += gridSpacing {
|
|
b.WriteString(fmt.Sprintf(` <line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f"/>`, areaX, y, areaX+areaW, y))
|
|
b.WriteString("\n")
|
|
}
|
|
b.WriteString("</g>\n")
|
|
|
|
// Major grid lines every 5 cells
|
|
majorSpacing := gridSpacing * 5
|
|
b.WriteString(`<g stroke="#aaa" stroke-width="0.3">` + "\n")
|
|
for x := areaX; x <= areaX+areaW+0.01; x += majorSpacing {
|
|
b.WriteString(fmt.Sprintf(` <line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f"/>`, x, areaY, x, areaY+areaH))
|
|
b.WriteString("\n")
|
|
}
|
|
for y := areaY; y <= areaY+areaH+0.01; y += majorSpacing {
|
|
b.WriteString(fmt.Sprintf(` <line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f"/>`, areaX, y, areaX+areaW, y))
|
|
b.WriteString("\n")
|
|
}
|
|
b.WriteString("</g>\n")
|
|
|
|
// Corner registration marks (L-shaped fiducials)
|
|
writeCornerMark(&b, areaX, areaY, markerSize, 1, 1)
|
|
writeCornerMark(&b, areaX+areaW, areaY, markerSize, -1, 1)
|
|
writeCornerMark(&b, areaX, areaY+areaH, markerSize, 1, -1)
|
|
writeCornerMark(&b, areaX+areaW, areaY+areaH, markerSize, -1, -1)
|
|
|
|
// Center crosshair
|
|
cx, cy := pageW/2, pageH/2
|
|
b.WriteString(fmt.Sprintf(`<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" stroke="black" stroke-width="0.3"/>`,
|
|
cx-markerSize, cy, cx+markerSize, cy))
|
|
b.WriteString("\n")
|
|
b.WriteString(fmt.Sprintf(`<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" stroke="black" stroke-width="0.3"/>`,
|
|
cx, cy-markerSize, cx, cy+markerSize))
|
|
b.WriteString("\n")
|
|
b.WriteString(fmt.Sprintf(`<circle cx="%.2f" cy="%.2f" r="1" fill="none" stroke="black" stroke-width="0.3"/>`,
|
|
cx, cy))
|
|
b.WriteString("\n")
|
|
|
|
// Multi-page alignment markers (only if multiple pages)
|
|
if totalCols > 1 || totalRows > 1 {
|
|
writeAlignmentMarkers(&b, areaX, areaY, areaW, areaH, markerSize)
|
|
|
|
// Page coordinate label
|
|
b.WriteString(fmt.Sprintf(`<text x="%.2f" y="%.2f" font-family="monospace" font-size="3" fill="#999" text-anchor="end">Page %d,%d of %dx%d</text>`,
|
|
areaX+areaW, areaY-2, col+1, row+1, totalCols, totalRows))
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
// Scale reference bar (10mm)
|
|
refY := areaY + areaH + 5
|
|
b.WriteString(fmt.Sprintf(`<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" stroke="black" stroke-width="0.5"/>`,
|
|
areaX, refY, areaX+10, refY))
|
|
b.WriteString("\n")
|
|
b.WriteString(fmt.Sprintf(`<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" stroke="black" stroke-width="0.5"/>`,
|
|
areaX, refY-1.5, areaX, refY+1.5))
|
|
b.WriteString("\n")
|
|
b.WriteString(fmt.Sprintf(`<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" stroke="black" stroke-width="0.5"/>`,
|
|
areaX+10, refY-1.5, areaX+10, refY+1.5))
|
|
b.WriteString("\n")
|
|
b.WriteString(fmt.Sprintf(`<text x="%.2f" y="%.2f" font-family="monospace" font-size="2.5" fill="black" text-anchor="middle">10mm</text>`,
|
|
areaX+5, refY+3.5))
|
|
b.WriteString("\n")
|
|
|
|
// Grid spacing label
|
|
b.WriteString(fmt.Sprintf(`<text x="%.2f" y="%.2f" font-family="monospace" font-size="2" fill="#999">Grid: %.0fmm</text>`,
|
|
areaX+15, refY+0.8, gridSpacing))
|
|
b.WriteString("\n")
|
|
|
|
b.WriteString("</svg>\n")
|
|
return b.String(), nil
|
|
}
|
|
|
|
func writeCornerMark(b *strings.Builder, x, y, size, dx, dy float64) {
|
|
// L-shaped corner mark
|
|
b.WriteString(fmt.Sprintf(`<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" stroke="black" stroke-width="0.5"/>`,
|
|
x, y, x+size*dx, y))
|
|
b.WriteString("\n")
|
|
b.WriteString(fmt.Sprintf(`<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" stroke="black" stroke-width="0.5"/>`,
|
|
x, y, x, y+size*dy))
|
|
b.WriteString("\n")
|
|
// Small filled circle at corner
|
|
b.WriteString(fmt.Sprintf(`<circle cx="%.2f" cy="%.2f" r="0.8" fill="black"/>`, x, y))
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
func writeAlignmentMarkers(b *strings.Builder, x, y, w, h, size float64) {
|
|
// Edge midpoint crosses for stitching alignment
|
|
edges := [][2]float64{
|
|
{x + w/2, y}, // top center
|
|
{x + w/2, y + h}, // bottom center
|
|
{x, y + h/2}, // left center
|
|
{x + w, y + h/2}, // right center
|
|
}
|
|
for _, e := range edges {
|
|
b.WriteString(fmt.Sprintf(`<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" stroke="black" stroke-width="0.4"/>`,
|
|
e[0]-size/2, e[1], e[0]+size/2, e[1]))
|
|
b.WriteString("\n")
|
|
b.WriteString(fmt.Sprintf(`<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" stroke="black" stroke-width="0.4"/>`,
|
|
e[0], e[1]-size/2, e[0], e[1]+size/2))
|
|
b.WriteString("\n")
|
|
}
|
|
}
|