package imaging import ( "fmt" "os/exec" ) // HEIC cannot be decoded in pure Go (HEVC), so conversion shells out to a // host-installed CLI. The Go binary itself stays cgo-free; the converter is // a runtime dependency (Debian: libvips-tools or libheif-examples). type HeicConverter struct { kind string // "vips" | "heif-convert" | "custom" path string } // DetectHeicConverter honors an explicit HEIC_CONVERTER command, else // autodetects vips then heif-convert. Returns nil when none is available; // uploads of HEIC are then rejected with a clear message. func DetectHeicConverter(explicit string) *HeicConverter { if explicit != "" { if p, err := exec.LookPath(explicit); err == nil { return &HeicConverter{kind: "custom", path: p} } return nil } if p, err := exec.LookPath("vips"); err == nil { return &HeicConverter{kind: "vips", path: p} } if p, err := exec.LookPath("heif-convert"); err == nil { return &HeicConverter{kind: "heif-convert", path: p} } return nil } func (h *HeicConverter) Name() string { return h.path } // ToJPEG converts src (a .heic file) to a JPEG at dst. func (h *HeicConverter) ToJPEG(src, dst string) error { var cmd *exec.Cmd switch h.kind { case "vips": cmd = exec.Command(h.path, "copy", src, dst+"[Q=95]") case "heif-convert": cmd = exec.Command(h.path, "-q", "95", src, dst) default: // custom converter contract: cmd = exec.Command(h.path, src, dst) } if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("heic conversion failed: %v: %s", err, truncate(string(out), 300)) } return nil } func truncate(s string, n int) string { if len(s) > n { return s[:n] } return s }