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(` `, pageW, pageH, pageW, pageH)) // White background b.WriteString(fmt.Sprintf(` `, 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(` `, areaX, areaY, areaW, areaH)) // Grid lines b.WriteString(`` + "\n") for x := areaX; x <= areaX+areaW+0.01; x += gridSpacing { b.WriteString(fmt.Sprintf(` `, x, areaY, x, areaY+areaH)) b.WriteString("\n") } for y := areaY; y <= areaY+areaH+0.01; y += gridSpacing { b.WriteString(fmt.Sprintf(` `, areaX, y, areaX+areaW, y)) b.WriteString("\n") } b.WriteString("\n") // Major grid lines every 5 cells majorSpacing := gridSpacing * 5 b.WriteString(`` + "\n") for x := areaX; x <= areaX+areaW+0.01; x += majorSpacing { b.WriteString(fmt.Sprintf(` `, x, areaY, x, areaY+areaH)) b.WriteString("\n") } for y := areaY; y <= areaY+areaH+0.01; y += majorSpacing { b.WriteString(fmt.Sprintf(` `, areaX, y, areaX+areaW, y)) b.WriteString("\n") } b.WriteString("\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(``, cx-markerSize, cy, cx+markerSize, cy)) b.WriteString("\n") b.WriteString(fmt.Sprintf(``, cx, cy-markerSize, cx, cy+markerSize)) b.WriteString("\n") b.WriteString(fmt.Sprintf(``, 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(`Page %d,%d of %dx%d`, 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(``, areaX, refY, areaX+10, refY)) b.WriteString("\n") b.WriteString(fmt.Sprintf(``, areaX, refY-1.5, areaX, refY+1.5)) b.WriteString("\n") b.WriteString(fmt.Sprintf(``, areaX+10, refY-1.5, areaX+10, refY+1.5)) b.WriteString("\n") b.WriteString(fmt.Sprintf(`10mm`, areaX+5, refY+3.5)) b.WriteString("\n") // Grid spacing label b.WriteString(fmt.Sprintf(`Grid: %.0fmm`, areaX+15, refY+0.8, gridSpacing)) b.WriteString("\n") b.WriteString("\n") return b.String(), nil } func writeCornerMark(b *strings.Builder, x, y, size, dx, dy float64) { // L-shaped corner mark b.WriteString(fmt.Sprintf(``, x, y, x+size*dx, y)) b.WriteString("\n") b.WriteString(fmt.Sprintf(``, x, y, x, y+size*dy)) b.WriteString("\n") // Small filled circle at corner b.WriteString(fmt.Sprintf(``, 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(``, e[0]-size/2, e[1], e[0]+size/2, e[1])) b.WriteString("\n") b.WriteString(fmt.Sprintf(``, e[0], e[1]-size/2, e[0], e[1]+size/2)) b.WriteString("\n") } }