package output import ( "crypto/sha256" "encoding/json" "fmt" "image" "image/jpeg" "io" "os" "path/filepath" "regexp" "strings" "materialanalyzer/internal/imageproc" "materialanalyzer/internal/model" ) func AssetDir(outputDir, sku string) string { return filepath.Join(outputDir, SafePathName(sku)) } func MaterialJSONPath(assetDir string) string { return filepath.Join(assetDir, "material.json") } func AssetExists(assetDir string) bool { _, err := os.Stat(MaterialJSONPath(assetDir)) return err == nil } func EnsureAssetDir(assetDir string) error { return os.MkdirAll(assetDir, 0o755) } func WriteImages(assetDir string, img image.Image) error { if err := EnsureAssetDir(assetDir); err != nil { return err } preview := imageproc.ResizeToMax(img, 900) if err := writeJPEG(filepath.Join(assetDir, "preview.jpg"), preview, 90); err != nil { return err } thumbnail := imageproc.ResizeToFill(img, 256, 256) return writeJPEG(filepath.Join(assetDir, "thumbnail.jpg"), thumbnail, 84) } func WriteHistogram(assetDir string, hist model.ColorHistogram) error { return writeJSON(filepath.Join(assetDir, "histogram.json"), hist) } func WriteMaterial(assetDir string, asset model.MaterialAsset) error { return writeJSON(MaterialJSONPath(assetDir), asset) } func WriteSemantic(assetDir string, semantic interface{}) error { return writeJSON(filepath.Join(assetDir, "semantic.json"), semantic) } func WriteManifest(assetDir string, manifest model.AssetManifest) error { if len(manifest.FileDetails) == 0 && len(manifest.Files) > 0 { details, err := BuildManifestFileDetails(assetDir, manifest.Files) if err != nil { return err } manifest.FileDetails = details } return writeJSON(filepath.Join(assetDir, "manifest.json"), manifest) } func BuildManifestFileDetails(assetDir string, files []string) ([]model.ManifestFile, error) { details := make([]model.ManifestFile, 0, len(files)) for _, name := range files { if name == "manifest.json" { continue } detail, err := manifestFileDetail(assetDir, name) if err != nil { return nil, err } details = append(details, detail) } return details, nil } func manifestFileDetail(assetDir, name string) (model.ManifestFile, error) { path := filepath.Join(assetDir, name) file, err := os.Open(path) if err != nil { return model.ManifestFile{}, fmt.Errorf("open manifest file %s: %w", name, err) } defer file.Close() info, err := file.Stat() if err != nil { return model.ManifestFile{}, fmt.Errorf("stat manifest file %s: %w", name, err) } hash := sha256.New() if _, err := io.Copy(hash, file); err != nil { return model.ManifestFile{}, fmt.Errorf("hash manifest file %s: %w", name, err) } return model.ManifestFile{ Path: name, SHA256: fmt.Sprintf("%x", hash.Sum(nil)), SizeBytes: info.Size(), CreatedTime: info.ModTime().UTC().Format("2006-01-02T15:04:05Z07:00"), }, nil } func WriteBenchmark(outputDir string, benchmark model.BatchBenchmark) error { if err := os.MkdirAll(outputDir, 0o755); err != nil { return err } return writeJSON(filepath.Join(outputDir, "benchmark.json"), benchmark) } func WriteFailures(outputDir string, report model.FailureReport) error { if err := os.MkdirAll(outputDir, 0o755); err != nil { return err } return writeJSON(filepath.Join(outputDir, "failures.json"), report) } func writeJPEG(path string, img image.Image, quality int) error { file, err := os.Create(path) if err != nil { return err } defer file.Close() return jpeg.Encode(file, img, &jpeg.Options{Quality: quality}) } func writeJSON(path string, v interface{}) error { file, err := os.Create(path) if err != nil { return err } defer file.Close() enc := json.NewEncoder(file) enc.SetIndent("", " ") enc.SetEscapeHTML(false) return enc.Encode(v) } var unsafePathChars = regexp.MustCompile(`[^a-zA-Z0-9._-]+`) func SafePathName(name string) string { name = strings.TrimSpace(name) if name == "" { return "unknown" } name = unsafePathChars.ReplaceAllString(name, "_") name = strings.Trim(name, "._- ") if name == "" { return "unknown" } if len(name) > 120 { name = name[:120] } return name }