github.com/kintar/etxt@v0.0.9/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  }