package font import ( "bytes" "fmt" "image/color" "io/fs" "log" "os" "path/filepath" "strings" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/text/v2" "golang.org/x/image/font/gofont/goregular" ) // FontRenderer handles text rendering with assets/fonts directory support type FontRenderer struct { face *text.GoTextFace } // NewFontRenderer creates a new font renderer using only assets/fonts directory func NewFontRenderer() *FontRenderer { var fontData []byte var fontSource string // 优先加载 assets/fonts 目录中的字体 if data, source := loadFromAssetsDirectory(); data != nil { fontData = data fontSource = source log.Printf("✅ 使用项目字体: %s", source) } else { // 如果没有找到项目字体,使用 Go 默认字体 fontData = goregular.TTF fontSource = "Go默认字体 (建议添加中文字体到 assets/fonts 目录)" log.Printf("⚠️ 未找到项目字体,使用默认字体。建议将中文字体放入 assets/fonts/ 目录") } // 创建字体源 source, err := createFontSource(fontData, fontSource) if err != nil { // 最终回退 - 使用 Go 默认字体 log.Printf("❌ 字体加载失败,回退到默认字体: %v", err) source, err = text.NewGoTextFaceSource(bytes.NewReader(goregular.TTF)) if err != nil { panic(fmt.Sprintf("无法加载默认字体: %v", err)) } fontSource = "Go默认字体 (回退)" } face := &text.GoTextFace{ Source: source, Size: 16, } log.Printf("🎨 字体系统初始化完成,使用: %s", fontSource) return &FontRenderer{ face: face, } } // loadFromAssetsDirectory 专门扫描 assets/fonts 目录并加载字体文件 func loadFromAssetsDirectory() ([]byte, string) { // 尝试多个可能的路径 possiblePaths := []string{ "assets/fonts", // 开发环境 "Contents/Resources/assets/fonts", // macOS 应用包内部 "../Resources/assets/fonts", // macOS 应用包相对路径 filepath.Join(getExecutableDir(), "assets/fonts"), // 可执行文件同目录 } var assetsPath string var found bool // 查找存在的资源路径 for _, path := range possiblePaths { if _, err := os.Stat(path); err == nil { assetsPath = path found = true log.Printf("📁 找到字体目录: %s", path) break } } if !found { log.Printf("📁 未找到 assets/fonts 目录,已尝试路径: %v", possiblePaths) return nil, "" } // 扫描目录下的所有字体文件 var fontFiles []string err := filepath.WalkDir(assetsPath, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() { ext := strings.ToLower(filepath.Ext(path)) if ext == ".ttf" || ext == ".ttc" || ext == ".otf" { fontFiles = append(fontFiles, path) } } return nil }) if err != nil { log.Printf("❌ 扫描字体目录失败: %v", err) return nil, "" } if len(fontFiles) == 0 { log.Printf("📂 字体目录为空,请添加字体文件 (.ttf, .ttc, .otf)") return nil, "" } log.Printf("🔍 在 %s 找到 %d 个字体文件", assetsPath, len(fontFiles)) // 字体优先级:微软雅黑 > 其他中文字体 > 任意字体 fontPriorities := [][]string{ {"msyh", "yahei", "microsoft"}, // 微软雅黑系列 {"noto", "source", "思源", "源"}, // 思源/Noto系列 {"pingfang", "hiragino", "苹方"}, // 苹果字体系列 {"simhei", "simkai", "simsun", "宋", "黑"}, // 系统中文字体 } // 按优先级查找字体 for _, priorityGroup := range fontPriorities { for _, keyword := range priorityGroup { for _, fontFile := range fontFiles { fileName := strings.ToLower(filepath.Base(fontFile)) if strings.Contains(fileName, keyword) { if data, err := tryLoadFont(fontFile); err == nil { log.Printf("🎯 找到优先字体 (%s): %s", keyword, fontFile) return data, fontFile } else { log.Printf("⚠️ 字体文件损坏或不支持: %s, 错误: %v", fontFile, err) } } } } } // 如果没有找到优先字体,使用第一个可用的字体 for _, fontFile := range fontFiles { if data, err := tryLoadFont(fontFile); err == nil { log.Printf("📝 使用可用字体: %s", fontFile) return data, fontFile } else { log.Printf("⚠️ 跳过不可用字体: %s, 错误: %v", fontFile, err) } } log.Printf("❌ 字体目录中没有可用的字体文件") return nil, "" } // getExecutableDir 获取可执行文件所在目录 func getExecutableDir() string { execPath, err := os.Executable() if err != nil { log.Printf("⚠️ 获取可执行文件路径失败: %v", err) return "." } return filepath.Dir(execPath) } // tryLoadFont 尝试加载字体文件并验证格式 func tryLoadFont(fontPath string) ([]byte, error) { data, err := os.ReadFile(fontPath) if err != nil { return nil, fmt.Errorf("读取字体文件失败: %w", err) } // 验证字体数据是否可以被解析 _, err = createFontSource(data, fontPath) if err != nil { return nil, fmt.Errorf("字体格式验证失败: %w", err) } return data, nil } // createFontSource 创建字体源,处理可能的collection错误 func createFontSource(fontData []byte, fontSource string) (*text.GoTextFaceSource, error) { // 尝试直接加载 source, err := text.NewGoTextFaceSource(bytes.NewReader(fontData)) if err != nil { // 如果是 collection 错误,提供更详细的信息 errStr := err.Error() if strings.Contains(errStr, "collection") { return nil, fmt.Errorf("TTC集合字体文件不支持,请使用TTF格式: %s", filepath.Base(fontSource)) } return nil, fmt.Errorf("字体文件格式错误: %w", err) } return source, nil } // DrawText draws text at the specified position with proper Chinese support func (fr *FontRenderer) DrawText(screen *ebiten.Image, str string, x, y float64, clr color.Color) { op := &text.DrawOptions{} op.GeoM.Translate(x, y) op.ColorScale.ScaleWithColor(clr) text.Draw(screen, str, fr.face, op) } // SetFontSize sets the font size func (fr *FontRenderer) SetFontSize(size float64) { fr.face.Size = size } // MeasureText returns text dimensions with better accuracy func (fr *FontRenderer) MeasureText(str string) (float64, float64) { width, _ := text.Measure(str, fr.face, 0) return width, fr.face.Size } // GetFontInfo returns information about the currently loaded font func (fr *FontRenderer) GetFontInfo() string { return fmt.Sprintf("字体大小: %.1f", fr.face.Size) }