package runner import ( "context" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strings" ) // InstallFont downloads a font file and installs it into the OS so After Effects // can use it. Returns nil on success. Best-effort per platform. func InstallFont(ctx context.Context, fileURL, name string) error { ext := filepath.Ext(strings.Split(fileURL, "?")[0]) if ext == "" { ext = ".ttf" } fileName := sanitizeFontName(name) + ext tmpDir, err := os.MkdirTemp("", "fr-font-") if err != nil { return fmt.Errorf("tmp dir: %w", err) } defer os.RemoveAll(tmpDir) tmp := filepath.Join(tmpDir, fileName) if _, err := DownloadFile(ctx, fileURL, tmp); err != nil { return fmt.Errorf("download: %w", err) } switch runtime.GOOS { case "windows": return installFontWindows(ctx, tmp, fileName) case "darwin": return copyFile(tmp, filepath.Join(os.Getenv("HOME"), "Library", "Fonts", fileName)) default: // linux dir := filepath.Join(os.Getenv("HOME"), ".local", "share", "fonts") _ = os.MkdirAll(dir, 0o755) if err := copyFile(tmp, filepath.Join(dir, fileName)); err != nil { return err } _ = exec.CommandContext(ctx, "fc-cache", "-f").Run() return nil } } func installFontWindows(ctx context.Context, src, fileName string) error { // Prefer the machine-wide Fonts dir; fall back to the per-user dir. dst := filepath.Join(os.Getenv("WINDIR"), "Fonts", fileName) if err := copyFile(src, dst); err != nil { userFonts := filepath.Join(os.Getenv("LOCALAPPDATA"), "Microsoft", "Windows", "Fonts") _ = os.MkdirAll(userFonts, 0o755) dst = filepath.Join(userFonts, fileName) if err := copyFile(src, dst); err != nil { return fmt.Errorf("copy font: %w", err) } } // Register so Windows/AE pick it up without a reboot (best-effort; needs admin). _ = exec.CommandContext(ctx, "reg", "add", `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts`, "/v", fileName, "/t", "REG_SZ", "/d", dst, "/f").Run() return nil } func copyFile(src, dst string) error { in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.Create(dst) if err != nil { return err } defer out.Close() if _, err := io.Copy(out, in); err != nil { return err } return out.Sync() } func sanitizeFontName(s string) string { s = strings.TrimSpace(s) repl := strings.NewReplacer("/", "_", "\\", "_", ":", "_", "*", "_", "?", "_", "\"", "_", "<", "_", ">", "_", "|", "_", " ", "_") s = repl.Replace(s) if s == "" { return "font" } return s }