package assets import ( "flag" "fmt" "io" "os" "path/filepath" "strings" ) var textureExts = map[string]bool{".dds": true, ".png": true, ".tga": true} // runConvert converts textures in place, flipping vertically exactly once per // conversion. --to defaults to dds. Files already in the target format are // skipped. func runConvert(args []string, stdout, stderr io.Writer) int { fs := flag.NewFlagSet("convert", flag.ContinueOnError) fs.SetOutput(stderr) to := fs.String("to", "dds", "target format: dds|png|tga") backend := fs.String("backend", "magick", "ImageMagick-compatible binary") nonRecursive := fs.Bool("non-recursive", false, "do not descend into subdirectories") if err := fs.Parse(args); err != nil { return exitUsage } target := strings.ToLower(*to) if target != "dds" && target != "png" && target != "tga" { fmt.Fprintln(stderr, "assets convert: --to must be dds, png, or tga") return exitUsage } dirs := fs.Args() if len(dirs) == 0 { fmt.Fprintln(stderr, "assets convert: usage: convert [--to dds|png|tga] ...") return exitUsage } magick := look(*backend) if magick == "" { fmt.Fprintf(stderr, "assets convert: backend %q not found — install ImageMagick (magick)\n", *backend) return exitTool } files, err := walk(dirs, textureExts, !*nonRecursive) if err != nil { fmt.Fprintln(stderr, "assets convert:", err) return exitUsage } targetExt := "." + target failed := false for _, src := range files { srcExt := strings.ToLower(filepath.Ext(src)) if srcExt == targetExt { continue // already in the target format } dst := strings.TrimSuffix(src, filepath.Ext(src)) + targetExt var convErr error if target == "dds" { convErr = pngToDDS(magick, src, dst) } else { convErr = ddsToPNG(magick, src, dst) // works for any decodable source } if convErr != nil { fmt.Fprintf(stderr, "assets convert: %s: %v\n", src, convErr) failed = true continue } if dst != src { if err := os.Remove(src); err != nil { fmt.Fprintf(stderr, "assets convert: %s: %v\n", src, err) failed = true } } } if failed { return exitFail } return exitOK } // ddsToPNG decodes src to dst (PNG/TGA by dst's extension), flipping vertically // once. The single flip is the defining NWN behavior. func ddsToPNG(magickBin, src, dst string) error { if out, err := runner("", nil, magickBin, src, "-flip", dst); err != nil { return fmt.Errorf("magick: %v: %s", err, strings.TrimSpace(string(out))) } return nil } // pngToDDS encodes src to a DDS at dst, flipping vertically once and choosing // DXT1 for opaque images, DXT5 for images with alpha. func pngToDDS(magickBin, src, dst string) error { compression := "dxt1" if hasAlpha(magickBin, src) { compression = "dxt5" } // ponytail: mipmaps rely on magick's default full chain (NWN wants a // pyramid, which magick writes by default). Add `-define dds:mipmaps=N` // here if a specific count is ever required. out, err := runner("", nil, magickBin, src, "-flip", "-define", "dds:compression="+compression, dst) if err != nil { return fmt.Errorf("magick: %v: %s", err, strings.TrimSpace(string(out))) } return nil } // hasAlpha reports whether src has any non-opaque pixel, via `magick identify`. // On any probe error it assumes alpha (DXT5), the safe/lossless-alpha default. func hasAlpha(magickBin, src string) bool { out, err := runner("", nil, magickBin, "identify", "-format", "%[opaque]", src) if err != nil { return true } return !strings.EqualFold(strings.TrimSpace(string(out)), "true") }