github.com/Kintar/etxt@v0.0.0-20221224033739-2fc69f000137/font_library.go (about) 1 package etxt 2 3 import "io/fs" 4 import "embed" 5 import "errors" 6 import "strings" 7 import "path/filepath" 8 9 // A collection of fonts accessible by name. 10 // 11 // The goal of a FontLibrary is to make it easy to load fonts in bulk 12 // and keep them all in a single place. 13 // 14 // FontLibrary doesn't know about system fonts, but there are other 15 // packages out there that can find those for you, if you are interested. 16 type FontLibrary struct { 17 fonts map[string]*Font 18 } 19 20 // Creates a new, empty font library. 21 func NewFontLibrary() *FontLibrary { 22 return &FontLibrary{ 23 fonts: make(map[string]*Font), 24 } 25 } 26 27 // Returns the current number of fonts in the library. 28 func (self *FontLibrary) Size() int { return len(self.fonts) } 29 30 // Returns the list of fonts currently loaded in the FontLibrary as a string. 31 // The result includes the font name and the amount of glyphs for each font. 32 // Mostly useful for debugging and discovering font names and families. 33 // func (self *FontLibrary) StringView() string { 34 // var strBuilder strings.Builder 35 // firstFont := true 36 // for name, font := range self.fonts { 37 // if firstFont { firstFont = false } else { strBuilder.WriteRune('\n') } 38 // strBuilder.WriteString("* " + name + " (" + strconv.Itoa(font.NumGlyphs()) + " glyphs)") 39 // } 40 // return strBuilder.String() 41 // } 42 43 // Finds out whether a font with the given name exists in the library. 44 func (self *FontLibrary) HasFont(name string) bool { 45 _, found := self.fonts[name] 46 return found 47 } 48 49 // Returns the font with the given name, or nil if not found. 50 // 51 // If you don't know what are the names of your fonts, there are a few 52 // ways to figure it out: 53 // - Load the fonts into the font library and print their names with 54 // [FontLibrary.EachFont]. 55 // - Use the [FontName]() function directly on a [*Font] object. 56 // - Open a font with the OS's default font viewer; the name is usually 57 // on the title and/or first line of text. 58 func (self *FontLibrary) GetFont(name string) *Font { 59 font, found := self.fonts[name] 60 if found { 61 return font 62 } 63 return nil 64 } 65 66 // Loads the given font into the library and returns its name and any 67 // possible error. If the given font is nil, the method will panic. If 68 // another font with the same name was already loaded, [ErrAlreadyLoaded] 69 // will be returned as the error. 70 // 71 // This method is rarely necessary unless the font loading is done 72 // by a third-party library. In general, using the FontLibrary.Parse*() 73 // functions is preferable. 74 func (self *FontLibrary) LoadFont(font *Font) (string, error) { 75 name, err := FontName(font) 76 if err != nil { 77 return "", err 78 } 79 return name, self.addNewFont(font, name) 80 } 81 82 // Returns false if the font can't be removed due to not being found. 83 // 84 // This function is rarely necessary unless your program also has some 85 // mechanism to keep adding fonts without limit. 86 // 87 // The given font name must match the name returned by the original font 88 // parsing function. Font names can also be recovered through 89 // [FontLibrary.EachFont]. 90 func (self *FontLibrary) RemoveFont(name string) bool { 91 _, found := self.fonts[name] 92 if !found { 93 return false 94 } 95 delete(self.fonts, name) 96 return true 97 } 98 99 // Returns the name of the added font and any possible error. 100 // If error == nil, the font name will be non-empty. 101 // 102 // If a font with the same name has already been loaded, 103 // [ErrAlreadyLoaded] will be returned. 104 func (self *FontLibrary) ParseFontFrom(path string) (string, error) { 105 font, name, err := ParseFontFrom(path) 106 if err != nil { 107 return name, err 108 } 109 return name, self.addNewFont(font, name) 110 } 111 112 // Similar to [FontLibrary.ParseFontFrom], but taking the font bytes 113 // directly. The font bytes may be gzipped. The bytes must not be 114 // modified while the font is in use. 115 func (self *FontLibrary) ParseFontBytes(fontBytes []byte) (string, error) { 116 font, name, err := ParseFontBytes(fontBytes) 117 if err != nil { 118 return name, err 119 } 120 return name, self.addNewFont(font, name) 121 } 122 123 var ErrAlreadyLoaded = errors.New("font already loaded") 124 125 func (self *FontLibrary) addNewFont(font *Font, name string) error { 126 if self.HasFont(name) { 127 return ErrAlreadyLoaded 128 } 129 self.fonts[name] = font 130 return nil 131 } 132 133 // Calls the given function for each font in the library, passing their 134 // names and content as arguments. 135 // 136 // If the given function returns a non-nil error, EachFont will immediately 137 // stop and return that error. Otherwise, EachFont will always return nil. 138 // 139 // Example code to print the names of all the fonts in the library: 140 // 141 // fontLib.EachFont(func(name string, _ *etxt.Font) error { 142 // fmt.Println(name) 143 // return nil 144 // }) 145 func (self *FontLibrary) EachFont(fontFunc func(string, *Font) error) error { 146 for name, font := range self.fonts { 147 err := fontFunc(name, font) 148 if err != nil { 149 return err 150 } 151 } 152 return nil 153 } 154 155 // Walks the given directory non-recursively and adds all the .ttf and .otf 156 // fonts in it. Returns the number of fonts added, the number of fonts skipped 157 // (a font with the same name already exists in the FontLibrary) and any error 158 // that might happen during the process. 159 func (self *FontLibrary) ParseDirFonts(dirName string) (int, int, error) { 160 absDirPath, err := filepath.Abs(dirName) 161 if err != nil { 162 return 0, 0, err 163 } 164 165 loaded, skipped := 0, 0 166 err = filepath.WalkDir(absDirPath, 167 func(path string, info fs.DirEntry, err error) error { 168 if err != nil { 169 return err 170 } 171 if info.IsDir() { 172 if path == absDirPath { 173 return nil 174 } 175 return fs.SkipDir 176 } 177 178 valid, _ := acceptFontPath(path) 179 if !valid { 180 return nil 181 } 182 _, err = self.ParseFontFrom(path) 183 if err == ErrAlreadyLoaded { 184 skipped += 1 185 return nil 186 } 187 if err == nil { 188 loaded += 1 189 } 190 return err 191 }) 192 return loaded, skipped, err 193 } 194 195 // Same as [FontLibrary.ParseDirFonts] but for embedded filesystems. 196 func (self *FontLibrary) ParseEmbedDirFonts(dirName string, embedFileSys embed.FS) (int, int, error) { 197 entries, err := embedFileSys.ReadDir(dirName) 198 if err != nil { 199 return 0, 0, err 200 } 201 202 if dirName == "." { 203 dirName = "" 204 } else if !strings.HasSuffix(dirName, "/") { 205 dirName += "/" 206 } 207 208 loaded, skipped := 0, 0 209 for _, entry := range entries { 210 if entry.IsDir() { 211 continue 212 } 213 path := dirName + entry.Name() 214 valid, _ := acceptFontPath(path) 215 if !valid { 216 continue 217 } 218 _, err = self.ParseEmbedFontFrom(path, embedFileSys) 219 if err == ErrAlreadyLoaded { 220 skipped += 1 221 continue 222 } 223 if err != nil { 224 return loaded, skipped, err 225 } 226 loaded += 1 227 } 228 return loaded, skipped, nil 229 } 230 231 // Same as [FontLibrary.ParseFontFrom] but for embedded filesystems. 232 func (self *FontLibrary) ParseEmbedFontFrom(path string, embedFileSys embed.FS) (string, error) { 233 font, name, err := ParseEmbedFontFrom(path, embedFileSys) 234 if err != nil { 235 return name, err 236 } 237 return name, self.addNewFont(font, name) 238 }