117 lines
3.5 KiB
Go
117 lines
3.5 KiB
Go
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] <dir>...")
|
|
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")
|
|
}
|